yggdrasil/components/skeletons/
notes_route_skeleton.rs1use leptos::prelude::*;
8
9use crate::components::skeletons::atoms::SkeletonBox;
10
11#[component]
13pub fn NotesStreamSkeleton() -> impl IntoView {
14 view! {
15 <div class="notes-stream-skeleton" aria-hidden="true">
16 <SkeletonBox class="h-3 w-24"/>
17 {(0..2)
18 .map(|_| {
19 view! {
20 <div class="note-moment">
21 <SkeletonBox class="h-3 w-10"/>
22 <div>
23 <SkeletonBox class="h-4 w-full"/>
24 <SkeletonBox class="h-4 mt-2 w-3/4"/>
25 </div>
26 </div>
27 }
28 })
29 .collect_view()}
30 <SkeletonBox class="h-36 w-full rounded-[10px] mt-3"/>
31 </div>
32 }
33}
34
35#[component]
37pub fn NotesPageSkeleton() -> impl IntoView {
38 view! {
39 <div class="notes" role="status" aria-label="正在加载笔记">
40 <div class="notes-header" aria-hidden="true">
41 <div>
42 <h1 class="notes-title">"笔记"</h1>
43 <SkeletonBox class="h-4 mt-3 w-72 max-w-full"/>
44 </div>
45 </div>
46 <div class="notes-layout" aria-hidden="true">
47 <div class="notes-side">
48 <SkeletonBox class="h-[34px] w-full rounded-lg"/>
49 <div class="notes-side-group">
50 {(0..3)
51 .map(|_| view! { <SkeletonBox class="h-8 w-full rounded-lg mt-1"/> })
52 .collect_view()}
53 </div>
54 </div>
55 <div class="notes-stream">
56 <NotesStreamSkeleton/>
57 </div>
58 </div>
59 </div>
60 }
61}
62
63#[component]
65pub fn NotebookSkeleton() -> impl IntoView {
66 view! {
67 <div role="status" aria-label="正在加载笔记本">
68 <div aria-hidden="true">
69 <SkeletonBox class="h-3 w-20"/>
70 <SkeletonBox class="h-9 mt-8 w-56"/>
71 <SkeletonBox class="h-4 mt-4 w-full"/>
72 <SkeletonBox class="h-10 mt-6 w-36 rounded-[10px]"/>
73 <div class="notebook-entries">
74 {(0..4)
75 .map(|_| {
76 view! {
77 <div class="notebook-entry">
78 <SkeletonBox class="h-5 w-7"/>
79 <div>
80 <SkeletonBox class="h-5 w-3/5"/>
81 <SkeletonBox class="h-3 mt-3 w-full"/>
82 </div>
83 </div>
84 }
85 })
86 .collect_view()}
87 </div>
88 </div>
89 </div>
90 }
91}
92
93#[component]
95pub fn NoteReaderSkeleton() -> impl IntoView {
96 view! {
97 <div class="notes-layout note-reader" role="status" aria-label="正在加载笔记">
98 <div class="notes-side note-rail" aria-hidden="true">
99 <SkeletonBox class="h-3 w-20"/>
100 {(0..4)
101 .map(|_| view! { <SkeletonBox class="h-7 w-full rounded-lg mt-2"/> })
102 .collect_view()}
103 </div>
104 <div class="note-document" aria-hidden="true">
105 <SkeletonBox class="h-5 w-24 rounded-full"/>
106 <SkeletonBox class="h-9 mt-4 w-4/5"/>
107 <SkeletonBox class="h-3 mt-4 w-48"/>
108 <div class="flex flex-col gap-3 mt-10">
109 {(0..6)
110 .map(|_| view! { <SkeletonBox class="h-3.5 w-full"/> })
111 .collect_view()}
112 </div>
113 </div>
114 </div>
115 }
116}
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
120enum NotesLayout {
121 List,
123 Notebook,
125 Note,
127}
128
129fn notes_layout(path: &str) -> NotesLayout {
133 let path = path.split(['?', '#']).next().unwrap_or_default();
134 let mut segments = path
135 .strip_prefix("/notes")
136 .unwrap_or_default()
137 .split('/')
138 .filter(|segment| !segment.is_empty());
139 match (segments.next(), segments.next(), segments.next()) {
140 (Some("book"), Some(_), None) => NotesLayout::Notebook,
141 (Some(_), None, _) => NotesLayout::Note,
142 _ => NotesLayout::List,
143 }
144}
145
146#[component]
148pub fn NotesRouteSkeleton(#[prop(into)] path: String) -> impl IntoView {
149 match notes_layout(&path) {
150 NotesLayout::Note => view! {
151 <div class="notes">
152 <NoteReaderSkeleton/>
153 </div>
154 }
155 .into_any(),
156 NotesLayout::Notebook => view! {
157 <div class="notebook">
158 <NotebookSkeleton/>
159 </div>
160 }
161 .into_any(),
162 NotesLayout::List => view! { <NotesPageSkeleton/> }.into_any(),
163 }
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169
170 #[test]
171 fn notes_layout_distinguishes_list_notebook_and_note() {
172 assert_eq!(notes_layout("/notes"), NotesLayout::List);
173 assert_eq!(notes_layout("/notes/"), NotesLayout::List);
174 assert_eq!(notes_layout("/notes/book/3"), NotesLayout::Notebook);
175 assert_eq!(notes_layout("/notes/hello"), NotesLayout::Note);
176 assert_eq!(notes_layout("/notes/hello?book=3"), NotesLayout::Note);
177 assert_eq!(notes_layout("/notes/book"), NotesLayout::Note);
178 assert_eq!(notes_layout("/notes/book/3/x"), NotesLayout::List);
179 }
180
181 #[test]
182 fn notes_route_skeleton_uses_the_matching_wrapper() {
183 let html = |path: &str| {
184 let path = path.to_string();
185 Owner::new().with(|| view! { <NotesRouteSkeleton path=path/> }.to_html())
186 };
187 assert!(html("/notes/book/3").contains(r#"class="notebook""#));
188 assert!(html("/notes/hello").contains("note-reader"));
189 assert!(html("/notes").contains("notes-header"));
190 }
191}