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:
parent
fce16288b5
commit
f9d23d1eed
161
input.css
161
input.css
@ -3,18 +3,20 @@
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
@theme {
|
||||
/* PaperMod Light Theme */
|
||||
--color-paper-theme: #ffffff;
|
||||
--font-serif: 'Source Serif 4', 'Noto Serif SC', 'Source Han Serif SC', 'Songti SC', Georgia, serif;
|
||||
|
||||
--color-paper-theme: #faf9f6;
|
||||
--color-paper-entry: #ffffff;
|
||||
--color-paper-primary: #1e1e1e;
|
||||
--color-paper-secondary: #6c6c6c;
|
||||
--color-paper-tertiary: #d6d6d6;
|
||||
--color-paper-content: #1f1f1f;
|
||||
--color-paper-code-block: #1c1d21;
|
||||
--color-paper-code-bg: #f5f5f5;
|
||||
--color-paper-border: #eeeeee;
|
||||
|
||||
/* PaperMod sizing */
|
||||
--color-paper-primary: #1c1917;
|
||||
--color-paper-secondary: #78716c;
|
||||
--color-paper-tertiary: #d6d3d1;
|
||||
--color-paper-content: #292524;
|
||||
--color-paper-code-block: #1c1917;
|
||||
--color-paper-code-bg: #f5f4f0;
|
||||
--color-paper-border: #e7e5e0;
|
||||
--color-paper-accent: #5c7a5e;
|
||||
--color-paper-accent-soft: #e8f0e8;
|
||||
|
||||
--radius-paper: 8px;
|
||||
--gap-paper: 24px;
|
||||
--content-gap-paper: 20px;
|
||||
@ -26,33 +28,36 @@
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
|
||||
body {
|
||||
background-color: var(--color-paper-theme);
|
||||
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;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--color-paper-theme: #1d1e20;
|
||||
--color-paper-entry: #2e2e33;
|
||||
--color-paper-primary: #dadadb;
|
||||
--color-paper-secondary: #9b9c9d;
|
||||
--color-paper-tertiary: #414244;
|
||||
--color-paper-content: #c4c4c5;
|
||||
--color-paper-code-block: #2e2e33;
|
||||
--color-paper-code-bg: #37383e;
|
||||
--color-paper-border: #333333;
|
||||
--color-paper-theme: #141416;
|
||||
--color-paper-entry: #1e1f22;
|
||||
--color-paper-primary: #e7e5df;
|
||||
--color-paper-secondary: #8c8981;
|
||||
--color-paper-tertiary: #3a3a3d;
|
||||
--color-paper-content: #d4d2cc;
|
||||
--color-paper-code-block: #1e1f22;
|
||||
--color-paper-code-bg: #28292d;
|
||||
--color-paper-border: #2a2b2e;
|
||||
--color-paper-accent: #7da97f;
|
||||
--color-paper-accent-soft: #1e2e1e;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
/* Post Single Layout */
|
||||
.post-single {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Post Header */
|
||||
.post-header {
|
||||
margin: 24px auto var(--content-gap-paper) auto;
|
||||
}
|
||||
@ -62,12 +67,14 @@
|
||||
line-height: 1.2;
|
||||
color: var(--color-paper-primary);
|
||||
word-break: break-word;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.post-description {
|
||||
margin-top: 10px;
|
||||
color: var(--color-paper-secondary);
|
||||
font-size: 16px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.post-meta {
|
||||
@ -91,7 +98,6 @@
|
||||
color: var(--color-paper-primary);
|
||||
}
|
||||
|
||||
/* Breadcrumbs */
|
||||
.breadcrumbs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@ -112,14 +118,12 @@
|
||||
text-underline-offset: 0.2rem;
|
||||
}
|
||||
|
||||
/* Draft Badge */
|
||||
.entry-hint {
|
||||
display: inline-flex;
|
||||
margin-left: 0.5rem;
|
||||
color: var(--color-paper-secondary);
|
||||
}
|
||||
|
||||
/* TOC */
|
||||
details.toc {
|
||||
margin-bottom: var(--content-gap-paper);
|
||||
background: var(--color-paper-code-bg);
|
||||
@ -174,7 +178,6 @@
|
||||
margin-inline-start: var(--gap-paper);
|
||||
}
|
||||
|
||||
/* TOC Animation */
|
||||
details {
|
||||
interpolate-size: allow-keywords;
|
||||
}
|
||||
@ -191,7 +194,6 @@
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Entry Cover */
|
||||
.entry-cover {
|
||||
margin-bottom: var(--content-gap-paper);
|
||||
}
|
||||
@ -202,23 +204,23 @@
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* Post Content */
|
||||
.post-content {
|
||||
color: var(--color-paper-content);
|
||||
margin: 30px 0;
|
||||
}
|
||||
|
||||
/* Markdown Content */
|
||||
.md-content h1 {
|
||||
margin: 40px auto 32px;
|
||||
font-size: 40px;
|
||||
color: var(--color-paper-primary);
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.md-content h2 {
|
||||
margin: 32px auto 24px;
|
||||
font-size: 32px;
|
||||
color: var(--color-paper-primary);
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.md-content h3 {
|
||||
@ -248,16 +250,16 @@
|
||||
.md-content a:not(.anchor) {
|
||||
text-underline-offset: 0.3rem;
|
||||
text-decoration: underline;
|
||||
color: var(--color-paper-primary);
|
||||
color: var(--color-paper-accent);
|
||||
}
|
||||
|
||||
.md-content a:not(.anchor):hover {
|
||||
color: var(--color-paper-secondary);
|
||||
color: #4a6249;
|
||||
}
|
||||
|
||||
.md-content p {
|
||||
margin-bottom: var(--content-gap-paper);
|
||||
line-height: 1.8;
|
||||
line-height: 1.85;
|
||||
}
|
||||
|
||||
.md-content p:last-child {
|
||||
@ -325,7 +327,7 @@
|
||||
|
||||
.md-content-img-zoomable {
|
||||
cursor: zoom-in;
|
||||
transition: opacity 0.2s ease;
|
||||
transition: opacity 0.2s ease-out;
|
||||
}
|
||||
|
||||
.md-content-img-zoomable:hover {
|
||||
@ -337,7 +339,6 @@
|
||||
border-radius: var(--radius-paper);
|
||||
}
|
||||
|
||||
/* Code Blocks */
|
||||
.md-content code {
|
||||
padding: 0.2rem 0.3rem;
|
||||
font-size: 0.85em;
|
||||
@ -366,12 +367,10 @@
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Reset display for syntax-highlighted spans to prevent Tailwind .block conflict */
|
||||
.md-content pre code span {
|
||||
display: inline !important;
|
||||
}
|
||||
|
||||
/* Copy Code Button */
|
||||
.copy-code {
|
||||
display: none;
|
||||
position: absolute;
|
||||
@ -386,11 +385,11 @@
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
font-family: inherit;
|
||||
transition: background 0.2s ease;
|
||||
transition: background 0.2s ease-out;
|
||||
}
|
||||
|
||||
.copy-code:hover {
|
||||
background: rgba(100, 100, 100, 0.9);
|
||||
background: rgba(92, 122, 94, 0.9);
|
||||
}
|
||||
|
||||
pre:hover .copy-code,
|
||||
@ -398,7 +397,6 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Heading Anchors */
|
||||
.anchor {
|
||||
display: none;
|
||||
text-decoration: none;
|
||||
@ -423,12 +421,10 @@
|
||||
color: var(--color-paper-primary) !important;
|
||||
}
|
||||
|
||||
/* Post Footer */
|
||||
.post-footer {
|
||||
margin-top: var(--content-gap-paper);
|
||||
}
|
||||
|
||||
/* Tags */
|
||||
.post-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@ -452,15 +448,15 @@
|
||||
border-radius: var(--radius-paper);
|
||||
border: 1px solid var(--color-paper-border);
|
||||
text-decoration: none;
|
||||
transition: background 0.2s ease;
|
||||
transition: all 0.2s ease-out;
|
||||
}
|
||||
|
||||
.post-tags a:hover {
|
||||
background: var(--color-paper-border);
|
||||
color: var(--color-paper-primary);
|
||||
background: var(--color-paper-accent-soft);
|
||||
border-color: var(--color-paper-accent);
|
||||
color: var(--color-paper-accent);
|
||||
}
|
||||
|
||||
/* Post Navigation */
|
||||
.paginav {
|
||||
display: flex;
|
||||
border-radius: var(--radius-paper);
|
||||
@ -477,7 +473,7 @@
|
||||
padding: 0.8rem;
|
||||
text-decoration: none;
|
||||
color: var(--color-paper-primary);
|
||||
transition: background 0.2s ease;
|
||||
transition: background 0.2s ease-out;
|
||||
}
|
||||
|
||||
.paginav a:hover {
|
||||
@ -491,7 +487,6 @@
|
||||
|
||||
.paginav .title {
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-paper-secondary);
|
||||
}
|
||||
@ -501,7 +496,6 @@
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Back to Home */
|
||||
.back-to-home {
|
||||
margin-top: calc(var(--content-gap-paper) * 1.5);
|
||||
padding-top: var(--content-gap-paper);
|
||||
@ -527,17 +521,16 @@
|
||||
text-underline-offset: 0.3rem;
|
||||
}
|
||||
|
||||
/* Image Viewer Lightbox */
|
||||
.image-viewer-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 9999;
|
||||
z-index: 50;
|
||||
background: rgba(0, 0, 0, 0.92);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
animation: fadeIn 0.2s ease;
|
||||
animation: fadeIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
.image-viewer-content {
|
||||
@ -568,24 +561,23 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background 0.2s ease;
|
||||
transition: background 0.2s ease-out;
|
||||
}
|
||||
|
||||
.image-viewer-close:hover {
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
/* Markdown Image Lightbox */
|
||||
.md-image-lightbox-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 9999;
|
||||
z-index: 50;
|
||||
background: rgba(0, 0, 0, 0.92);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
animation: fadeIn 0.2s ease;
|
||||
animation: fadeIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
.md-image-lightbox-content {
|
||||
@ -616,14 +608,13 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background 0.2s ease;
|
||||
transition: background 0.2s ease-out;
|
||||
}
|
||||
|
||||
.md-image-lightbox-close:hover {
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
/* Post Card Cover Thumbnail */
|
||||
.post-card-cover {
|
||||
overflow: hidden;
|
||||
border-radius: var(--radius-paper);
|
||||
@ -633,12 +624,60 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s ease;
|
||||
transition: transform 0.3s ease-out;
|
||||
}
|
||||
|
||||
.post-card-cover:hover img {
|
||||
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 {
|
||||
@ -646,7 +685,6 @@
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media screen and (max-width: 768px) {
|
||||
.post-title {
|
||||
font-size: 32px;
|
||||
@ -686,7 +724,7 @@
|
||||
@keyframes pageEnter {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(4px);
|
||||
transform: translateY(6px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
@ -698,7 +736,6 @@
|
||||
animation: pageEnter 0.15s ease-out;
|
||||
}
|
||||
|
||||
/* Reduced Motion */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html {
|
||||
scroll-behavior: auto;
|
||||
|
||||
@ -67,7 +67,7 @@ pub fn Footer() -> Element {
|
||||
});
|
||||
|
||||
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() {
|
||||
format!("{} opacity-100 translate-y-0", base)
|
||||
} else {
|
||||
@ -76,9 +76,9 @@ pub fn Footer() -> Element {
|
||||
});
|
||||
|
||||
rsx! {
|
||||
footer { class: "w-full border-t border-gray-200 dark:border-[#333] 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]",
|
||||
span { "© 2026 Yggdrasil Blog" }
|
||||
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-paper-secondary",
|
||||
span { "© 2026 Yggdrasil" }
|
||||
}
|
||||
}
|
||||
a {
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
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]
|
||||
pub fn FormInput(
|
||||
@ -31,7 +31,7 @@ pub fn FormInput(
|
||||
#[component]
|
||||
pub fn FormLabel(label: &'static str) -> Element {
|
||||
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}"
|
||||
}
|
||||
}
|
||||
@ -42,7 +42,7 @@ pub fn AlertBox(message: String, variant: &'static str) -> Element {
|
||||
let (bg_class, text_class) = match variant {
|
||||
"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"),
|
||||
_ => ("bg-gray-100 dark:bg-[#333]", "text-gray-700 dark:text-[#9b9c9d]"),
|
||||
_ => ("bg-paper-code-bg", "text-paper-secondary"),
|
||||
};
|
||||
rsx! {
|
||||
div { class: "mb-4 p-3 {bg_class} {text_class} rounded-lg text-center",
|
||||
|
||||
@ -29,7 +29,7 @@ pub fn FrontendLayout() -> Element {
|
||||
let nav_items = use_nav_items(route.clone());
|
||||
|
||||
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 {} } }
|
||||
main { class: "flex-1 w-full max-w-3xl mx-auto px-6 py-6",
|
||||
SuspenseBoundary {
|
||||
|
||||
@ -13,10 +13,10 @@ pub struct NavItemConfig {
|
||||
#[component]
|
||||
pub fn Header(nav_items: Vec<NavItemConfig>, right_content: Element) -> Element {
|
||||
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",
|
||||
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 {},
|
||||
"Yggdrasil"
|
||||
}
|
||||
@ -39,12 +39,12 @@ pub fn Header(nav_items: Vec<NavItemConfig>, right_content: Element) -> Element
|
||||
|
||||
#[component]
|
||||
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 {
|
||||
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 {
|
||||
format!(
|
||||
"{} text-gray-600 dark:text-[#9b9c9d] hover:text-gray-900 dark:hover:text-[#dadadb]",
|
||||
"{} text-paper-secondary hover:text-paper-primary",
|
||||
base_class
|
||||
)
|
||||
};
|
||||
|
||||
@ -34,7 +34,7 @@ pub fn PostFooter(post: Post) -> Element {
|
||||
div { class: "back-to-home",
|
||||
Link {
|
||||
to: Route::Home {},
|
||||
"← Back to Home"
|
||||
"← 返回首页"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@ pub fn PostCard(post: Post) -> Element {
|
||||
|
||||
rsx! {
|
||||
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 {
|
||||
class: "block group",
|
||||
to: Route::PostDetail { slug: post_slug },
|
||||
@ -29,22 +29,22 @@ pub fn PostCard(post: Post) -> Element {
|
||||
}
|
||||
}
|
||||
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}"
|
||||
}
|
||||
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()}"
|
||||
}
|
||||
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}" }
|
||||
if !post.tags.is_empty() {
|
||||
span { "·" }
|
||||
for tag in post.tags.clone().into_iter() {
|
||||
span {
|
||||
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() },
|
||||
onclick: move |evt: dioxus::events::MouseEvent| evt.stop_propagation(),
|
||||
"{tag}"
|
||||
|
||||
@ -4,7 +4,7 @@ use dioxus::prelude::*;
|
||||
pub fn SkeletonBox(class: &'static str, style: Option<&'static str>) -> Element {
|
||||
rsx! {
|
||||
div {
|
||||
class: "bg-gray-200 dark:bg-[#2a2a2a] animate-pulse {class}",
|
||||
class: "bg-paper-tertiary/30 animate-pulse {class}",
|
||||
style: style.unwrap_or(""),
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,14 +4,14 @@ use dioxus::prelude::*;
|
||||
pub fn About() -> Element {
|
||||
rsx! {
|
||||
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 { "它使用 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",
|
||||
li { "Rust + Dioxus 0.7 (全栈 Web 框架)" }
|
||||
li { "PostgreSQL + tokio-postgres (数据库)" }
|
||||
@ -19,7 +19,7 @@ pub fn About() -> Element {
|
||||
li { "Tiptap Editor (富文本编辑器)" }
|
||||
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",
|
||||
li { "Markdown 写作与实时预览" }
|
||||
li { "文章标签与归档" }
|
||||
|
||||
@ -81,7 +81,7 @@ fn group_posts(posts: &[Post]) -> Vec<YearGroup> {
|
||||
pub fn Archives() -> Element {
|
||||
rsx! {
|
||||
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 })) => {
|
||||
let grouped = group_posts(posts);
|
||||
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() {
|
||||
@ -134,14 +134,14 @@ fn YearSection(year_group: YearGroup) -> Element {
|
||||
rsx! {
|
||||
div { class: "archive-year mt-10",
|
||||
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}",
|
||||
a {
|
||||
class: "archive-header-link hover:opacity-80 transition-opacity",
|
||||
href: "#{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() {
|
||||
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();
|
||||
|
||||
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 {
|
||||
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}",
|
||||
a {
|
||||
class: "archive-header-link hover:opacity-80 transition-opacity",
|
||||
href: "#{year}-{month_group.month_en}",
|
||||
"{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",
|
||||
for post in month_group.posts.iter() {
|
||||
@ -181,10 +181,10 @@ fn ArchiveEntry(post: Post) -> Element {
|
||||
|
||||
rsx! {
|
||||
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}"
|
||||
}
|
||||
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}"
|
||||
}
|
||||
Link {
|
||||
|
||||
@ -40,7 +40,7 @@ fn HomePosts(current_page: i32) -> Element {
|
||||
PostCard { post: post.clone() }
|
||||
}
|
||||
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 {
|
||||
rsx! {
|
||||
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"
|
||||
}
|
||||
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",
|
||||
if has_prev {
|
||||
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,
|
||||
span { class: "mr-1", "«" }
|
||||
"上一页"
|
||||
@ -100,7 +100,7 @@ fn Pagination(current_page: i32, total: i64) -> Element {
|
||||
}
|
||||
if has_next {
|
||||
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 },
|
||||
"下一页"
|
||||
span { class: "ml-1", "»" }
|
||||
|
||||
@ -51,9 +51,9 @@ pub fn Login() -> Element {
|
||||
});
|
||||
|
||||
rsx! {
|
||||
div { class: "min-h-screen flex items-center justify-center bg-white dark:bg-[#1d1e20]",
|
||||
div { class: "w-full max-w-md p-8 bg-white dark:bg-[#2e2e33] rounded-2xl border border-gray-200 dark:border-[#333]",
|
||||
h1 { class: "text-2xl font-bold text-center text-gray-900 dark:text-[#dadadb] mb-6",
|
||||
div { class: "min-h-screen flex items-center justify-center bg-paper-theme",
|
||||
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-paper-primary mb-6",
|
||||
"登录"
|
||||
}
|
||||
|
||||
@ -88,7 +88,7 @@ pub fn Login() -> Element {
|
||||
"登录"
|
||||
}
|
||||
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 {},
|
||||
"还没有账号?去注册"
|
||||
}
|
||||
|
||||
@ -51,19 +51,19 @@ pub fn Register() -> Element {
|
||||
};
|
||||
|
||||
rsx! {
|
||||
div { class: "min-h-screen flex items-center justify-center bg-white dark:bg-[#1d1e20]",
|
||||
div { class: "w-full max-w-md p-8 bg-white dark:bg-[#2e2e33] rounded-2xl border border-gray-200 dark:border-[#333]",
|
||||
h1 { class: "text-2xl font-bold text-center text-gray-900 dark:text-[#dadadb] mb-2",
|
||||
div { class: "min-h-screen flex items-center justify-center bg-paper-theme",
|
||||
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-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() {
|
||||
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 {},
|
||||
"去登录"
|
||||
}
|
||||
@ -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 {},
|
||||
"去登录"
|
||||
}
|
||||
|
||||
@ -26,14 +26,14 @@ pub fn Search() -> Element {
|
||||
|
||||
rsx! {
|
||||
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: "flex gap-2",
|
||||
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",
|
||||
placeholder: "输入关键词搜索文章...",
|
||||
value: query(),
|
||||
@ -41,7 +41,7 @@ pub fn Search() -> Element {
|
||||
onkeydown: move |e| if e.key() == Key::Enter { on_search() },
|
||||
}
|
||||
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(),
|
||||
"搜索"
|
||||
}
|
||||
@ -51,7 +51,7 @@ pub fn Search() -> Element {
|
||||
DelayedSkeleton { SearchSkeleton {} }
|
||||
} else if let Some(Ok(PostListResponse { posts, total: _ })) = search_res() {
|
||||
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 {
|
||||
|
||||
@ -11,7 +11,7 @@ use crate::router::Route;
|
||||
pub fn Tags() -> Element {
|
||||
rsx! {
|
||||
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)) => {
|
||||
let total = tags.iter().map(|t| t.post_count).sum::<i64>();
|
||||
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",
|
||||
for tag in tags {
|
||||
li {
|
||||
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() },
|
||||
"{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 {
|
||||
rsx! {
|
||||
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}"
|
||||
}
|
||||
}
|
||||
@ -92,9 +92,9 @@ fn TagDetailContent(tag: String) -> Element {
|
||||
match posts_data {
|
||||
Some(Ok((posts, total))) => {
|
||||
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() {
|
||||
|
||||
@ -135,7 +135,7 @@ pub fn ThemeToggle() -> Element {
|
||||
|
||||
rsx! {
|
||||
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()),
|
||||
if mounted() && theme() == Theme::Dark {
|
||||
svg {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user