vomebook commited on
Commit
08e78fb
·
verified ·
1 Parent(s): e0e151a

Upload 3 files

Browse files
Files changed (3) hide show
  1. static/app.js +61 -3
  2. static/index.html +2 -2
  3. static/style.css +95 -21
static/app.js CHANGED
@@ -67,6 +67,14 @@ const snippetQueue = [];
67
  const snippetCache = new Map();
68
  const snippetControllers = new Set();
69
 
 
 
 
 
 
 
 
 
70
  function themeIcon(isDark) {
71
  const shape = isDark
72
  ? '<path d="M20 15.5A8 8 0 1 1 8.5 4 6.5 6.5 0 0 0 20 15.5Z"/>'
@@ -464,11 +472,18 @@ function updateFilterVisibility() {
464
  }
465
 
466
  function showToast(message, duration = 2000) {
 
 
 
467
  DOM.toast.textContent = message;
468
  DOM.toast.style.display = "";
469
- clearTimeout(showToast._timer);
470
  showToast._timer = setTimeout(() => {
471
- DOM.toast.style.display = "none";
 
 
 
 
 
472
  }, duration);
473
  }
474
 
@@ -534,9 +549,19 @@ function applyTheme() {
534
  }
535
 
536
  function toggleTheme() {
 
 
537
  STATE.isDark = !STATE.isDark;
538
  applyTheme();
539
  localStorage.setItem("theme", STATE.isDark ? "dark" : "light");
 
 
 
 
 
 
 
 
540
  }
541
 
542
  function applyMobileMode() {
@@ -765,6 +790,7 @@ async function applyInitialSearch(sequence) {
765
  STATE.results = data.results || [];
766
  STATE.isLoading = false;
767
  DOM.resultsLoading.style.display = "none";
 
768
  renderResults();
769
  updateStatus();
770
  DOM.resultsContainer.scrollTop = 0;
@@ -810,6 +836,12 @@ function prefetchNextPage() {
810
  });
811
  }
812
 
 
 
 
 
 
 
813
  async function doSearch() {
814
  if (STATE.searchController) STATE.searchController.abort();
815
  abortSearchPrefetch();
@@ -828,6 +860,7 @@ async function doSearch() {
828
  STATE.searchController = controller;
829
  STATE.isLoading = true;
830
  DOM.resultsLoading.style.display = "flex";
 
831
  if (STATE.page === 1) {
832
  STATE.suppressAutoLoad = true;
833
  DOM.previewPanel.style.display = "none";
@@ -859,6 +892,7 @@ async function doSearch() {
859
  STATE.searchController = null;
860
  STATE.isLoading = false;
861
  DOM.resultsLoading.style.display = "none";
 
862
  requestAnimationFrame(() => { STATE.suppressAutoLoad = false; });
863
  prefetchNextPage();
864
  }
@@ -902,6 +936,7 @@ function appendResults(items) {
902
  if (!items.length) return;
903
  DOM.emptyState.style.display = "none";
904
  DOM.resultsList.insertAdjacentHTML("beforeend", items.map(resultItemHtml).join(""));
 
905
  DOM.multiActionBar.style.display = STATE.multiSelect ? "" : "none";
906
  observeResultSnippets();
907
  }
@@ -917,10 +952,23 @@ function renderResults() {
917
  }
918
  DOM.emptyState.style.display = "none";
919
  DOM.resultsList.innerHTML = STATE.results.map(resultItemHtml).join("");
 
920
  DOM.multiActionBar.style.display = STATE.multiSelect ? "" : "none";
921
  observeResultSnippets();
922
  }
923
 
 
 
 
 
 
 
 
 
 
 
 
 
924
  function resetSnippetObserver() {
925
  if (snippetObserver) snippetObserver.disconnect();
926
  snippetObserver = null;
@@ -1036,6 +1084,14 @@ async function openPreview(docId, keyword, options = {}) {
1036
  else previewBody.textContent = rawText;
1037
  DOM.previewPanel.style.display = STATE.isMobile ? "flex" : "block";
1038
  document.body.classList.add("preview-open");
 
 
 
 
 
 
 
 
1039
  STATE.previewDocId = docId;
1040
  if (options.scroll !== false) DOM.previewPanel.scrollIntoView({ behavior: "smooth", block: "start" });
1041
  } catch (error) {
@@ -1282,6 +1338,7 @@ function renderFolderTreeRows(animation = {}) {
1282
  container.innerHTML = "";
1283
  renderFolderNodes(container, STATE.folderTree, 0);
1284
  refreshFolderTreeSelectionState();
 
1285
  const newRows = container.querySelectorAll(".filter-folder-item[data-folder-path]");
1286
  newRows.forEach((row) => {
1287
  const path = row.dataset.folderPath;
@@ -1327,7 +1384,8 @@ function renderFolderTreeRows(animation = {}) {
1327
  function animateFolderCollapse(nodePath, exitingPaths) {
1328
  const container = DOM.filterFolderTree;
1329
  if (!container) return 200;
1330
- const duration = 200;
 
1331
  const glyph = container.querySelector(`.filter-folder-item[data-folder-path="${CSS.escape(nodePath)}"] .tree-toggle-glyph`);
1332
  if (glyph) {
1333
  glyph.animate([
 
67
  const snippetCache = new Map();
68
  const snippetControllers = new Set();
69
 
70
+ function motionAllowed() {
71
+ return !window.matchMedia("(prefers-reduced-motion: reduce)").matches;
72
+ }
73
+
74
+ function motionDuration(duration) {
75
+ return motionAllowed() ? duration : 0;
76
+ }
77
+
78
  function themeIcon(isDark) {
79
  const shape = isDark
80
  ? '<path d="M20 15.5A8 8 0 1 1 8.5 4 6.5 6.5 0 0 0 20 15.5Z"/>'
 
472
  }
473
 
474
  function showToast(message, duration = 2000) {
475
+ clearTimeout(showToast._timer);
476
+ clearTimeout(showToast._hideTimer);
477
+ DOM.toast.classList.remove("is-leaving");
478
  DOM.toast.textContent = message;
479
  DOM.toast.style.display = "";
 
480
  showToast._timer = setTimeout(() => {
481
+ const exitDuration = motionDuration(140);
482
+ if (exitDuration) DOM.toast.classList.add("is-leaving");
483
+ showToast._hideTimer = setTimeout(() => {
484
+ DOM.toast.style.display = "none";
485
+ DOM.toast.classList.remove("is-leaving");
486
+ }, exitDuration);
487
  }, duration);
488
  }
489
 
 
549
  }
550
 
551
  function toggleTheme() {
552
+ const animate = motionAllowed();
553
+ if (animate) document.body.classList.add("theme-transitioning");
554
  STATE.isDark = !STATE.isDark;
555
  applyTheme();
556
  localStorage.setItem("theme", STATE.isDark ? "dark" : "light");
557
+ if (animate) {
558
+ document.body.classList.remove("theme-switching");
559
+ void document.body.offsetWidth;
560
+ document.body.classList.add("theme-switching");
561
+ window.setTimeout(() => {
562
+ document.body.classList.remove("theme-transitioning", "theme-switching");
563
+ }, 280);
564
+ }
565
  }
566
 
567
  function applyMobileMode() {
 
790
  STATE.results = data.results || [];
791
  STATE.isLoading = false;
792
  DOM.resultsLoading.style.display = "none";
793
+ setSearchMotion(false);
794
  renderResults();
795
  updateStatus();
796
  DOM.resultsContainer.scrollTop = 0;
 
836
  });
837
  }
838
 
839
+ function setSearchMotion(loading, refreshing = false) {
840
+ DOM.resultsContainer.classList.toggle("is-searching", loading);
841
+ DOM.resultsContainer.classList.toggle("is-refreshing", loading && refreshing);
842
+ DOM.resultsContainer.setAttribute("aria-busy", loading ? "true" : "false");
843
+ }
844
+
845
  async function doSearch() {
846
  if (STATE.searchController) STATE.searchController.abort();
847
  abortSearchPrefetch();
 
860
  STATE.searchController = controller;
861
  STATE.isLoading = true;
862
  DOM.resultsLoading.style.display = "flex";
863
+ setSearchMotion(true, STATE.page === 1 && STATE.results.length > 0);
864
  if (STATE.page === 1) {
865
  STATE.suppressAutoLoad = true;
866
  DOM.previewPanel.style.display = "none";
 
892
  STATE.searchController = null;
893
  STATE.isLoading = false;
894
  DOM.resultsLoading.style.display = "none";
895
+ setSearchMotion(false);
896
  requestAnimationFrame(() => { STATE.suppressAutoLoad = false; });
897
  prefetchNextPage();
898
  }
 
936
  if (!items.length) return;
937
  DOM.emptyState.style.display = "none";
938
  DOM.resultsList.insertAdjacentHTML("beforeend", items.map(resultItemHtml).join(""));
939
+ animateResultRows(Array.from(DOM.resultsList.children).slice(-items.length));
940
  DOM.multiActionBar.style.display = STATE.multiSelect ? "" : "none";
941
  observeResultSnippets();
942
  }
 
952
  }
953
  DOM.emptyState.style.display = "none";
954
  DOM.resultsList.innerHTML = STATE.results.map(resultItemHtml).join("");
955
+ animateResultRows(Array.from(DOM.resultsList.children).slice(0, 8));
956
  DOM.multiActionBar.style.display = STATE.multiSelect ? "" : "none";
957
  observeResultSnippets();
958
  }
959
 
960
+ function animateResultRows(rows) {
961
+ if (!motionAllowed()) return;
962
+ rows.slice(0, 8).forEach((row, index) => {
963
+ row.style.setProperty("--result-enter-delay", `${index * 14}ms`);
964
+ row.classList.add("result-enter");
965
+ row.addEventListener("animationend", () => {
966
+ row.classList.remove("result-enter");
967
+ row.style.removeProperty("--result-enter-delay");
968
+ }, { once: true });
969
+ });
970
+ }
971
+
972
  function resetSnippetObserver() {
973
  if (snippetObserver) snippetObserver.disconnect();
974
  snippetObserver = null;
 
1084
  else previewBody.textContent = rawText;
1085
  DOM.previewPanel.style.display = STATE.isMobile ? "flex" : "block";
1086
  document.body.classList.add("preview-open");
1087
+ if (motionAllowed()) {
1088
+ DOM.previewPanel.classList.remove("preview-enter");
1089
+ void DOM.previewPanel.offsetWidth;
1090
+ DOM.previewPanel.classList.add("preview-enter");
1091
+ DOM.previewPanel.addEventListener("animationend", () => {
1092
+ DOM.previewPanel.classList.remove("preview-enter");
1093
+ }, { once: true });
1094
+ }
1095
  STATE.previewDocId = docId;
1096
  if (options.scroll !== false) DOM.previewPanel.scrollIntoView({ behavior: "smooth", block: "start" });
1097
  } catch (error) {
 
1338
  container.innerHTML = "";
1339
  renderFolderNodes(container, STATE.folderTree, 0);
1340
  refreshFolderTreeSelectionState();
1341
+ if (!motionAllowed()) return;
1342
  const newRows = container.querySelectorAll(".filter-folder-item[data-folder-path]");
1343
  newRows.forEach((row) => {
1344
  const path = row.dataset.folderPath;
 
1384
  function animateFolderCollapse(nodePath, exitingPaths) {
1385
  const container = DOM.filterFolderTree;
1386
  if (!container) return 200;
1387
+ const duration = motionDuration(200);
1388
+ if (!duration) return 0;
1389
  const glyph = container.querySelector(`.filter-folder-item[data-folder-path="${CSS.escape(nodePath)}"] .tree-toggle-glyph`);
1390
  if (glyph) {
1391
  glyph.animate([
static/index.html CHANGED
@@ -68,7 +68,7 @@
68
  <button id="multi-zip-download" class="multi-action-btn primary">合并下载</button>
69
  <span id="multi-selected-count" class="multi-selected-count"></span>
70
  </div>
71
- <div id="results-container" class="results-container">
72
  <div id="results-list" class="results-list"></div>
73
  <div id="results-loading" class="loading-spinner" style="display:none"><div class="spinner"></div></div>
74
  <div id="empty-state" class="empty-state" style="display:none">
@@ -145,7 +145,7 @@
145
 
146
  </footer>
147
  <div id="overlay" class="overlay" style="display:none"></div>
148
- <div id="toast" class="toast" style="display:none"></div>
149
 
150
  <script>if ("serviceWorker" in navigator) { navigator.serviceWorker.register("/sw.js", { scope: "/" }); }</script>
151
  <script src="/static/app.js"></script>
 
68
  <button id="multi-zip-download" class="multi-action-btn primary">合并下载</button>
69
  <span id="multi-selected-count" class="multi-selected-count"></span>
70
  </div>
71
+ <div id="results-container" class="results-container" aria-busy="false">
72
  <div id="results-list" class="results-list"></div>
73
  <div id="results-loading" class="loading-spinner" style="display:none"><div class="spinner"></div></div>
74
  <div id="empty-state" class="empty-state" style="display:none">
 
145
 
146
  </footer>
147
  <div id="overlay" class="overlay" style="display:none"></div>
148
+ <div id="toast" class="toast" role="status" aria-live="polite" style="display:none"></div>
149
 
150
  <script>if ("serviceWorker" in navigator) { navigator.serviceWorker.register("/sw.js", { scope: "/" }); }</script>
151
  <script src="/static/app.js"></script>
static/style.css CHANGED
@@ -35,7 +35,9 @@
35
  --shadow-sm: 0 1px 2px rgba(0,0,0,0.3);
36
  --shadow-md: 0 2px 8px rgba(0,0,0,0.4);
37
  --shadow-lg: 0 4px 16px rgba(0,0,0,0.5);
38
- --transition: 150ms ease;
 
 
39
  --sidebar-w: 280px;
40
  --sidebar-expanded-w: min(50vw, 720px);
41
  color-scheme: dark;
@@ -279,6 +281,13 @@ ul, li {
279
  max-height: 320px;
280
  overflow-y: auto;
281
  padding: 4px 0;
 
 
 
 
 
 
 
282
  }
283
 
284
  .search-history-dropdown .history-item {
@@ -461,6 +470,15 @@ ul, li {
461
  overflow: hidden;
462
  }
463
 
 
 
 
 
 
 
 
 
 
464
  body.preview-open {
465
  user-select: none;
466
  -webkit-user-select: none;
@@ -518,6 +536,15 @@ body.preview-open #preview-panel .preview-header {
518
  display: block;
519
  }
520
 
 
 
 
 
 
 
 
 
 
521
  .preview-title {
522
  font-size: 15px;
523
  font-weight: 600;
@@ -607,7 +634,7 @@ body.preview-open #preview-panel .preview-header {
607
  }
608
 
609
  .sidebar.collapsed {
610
- width: 0 !important;
611
  border-right: none;
612
  }
613
 
@@ -1154,6 +1181,40 @@ body.mobile .multi-toggle { padding: 1px 4px; font-size: 11px; }
1154
  flex: 1;
1155
  overflow-y: auto;
1156
  overflow-x: hidden;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1157
  }
1158
 
1159
  .snippet-loading {
@@ -1174,6 +1235,16 @@ body.mobile .multi-toggle { padding: 1px 4px; font-size: 11px; }
1174
  transition: background var(--transition);
1175
  }
1176
 
 
 
 
 
 
 
 
 
 
 
1177
  .result-item:nth-child(even) {
1178
  background: var(--surface-variant);
1179
  }
@@ -1762,6 +1833,10 @@ body.mobile .mobile-selected-count {
1762
  animation: toast-in 0.2s ease;
1763
  }
1764
 
 
 
 
 
1765
  @keyframes toast-in {
1766
  from {
1767
  opacity: 0;
@@ -1774,6 +1849,11 @@ body.mobile .mobile-selected-count {
1774
  }
1775
  }
1776
 
 
 
 
 
 
1777
  body.mobile .sidebar {
1778
  position: fixed;
1779
  top: var(--header-h);
@@ -1781,7 +1861,7 @@ body.mobile .sidebar {
1781
  z-index: 95;
1782
  width: 85vw !important;
1783
  max-width: 320px;
1784
- transition: transform var(--transition), opacity var(--transition);
1785
  will-change: transform;
1786
  }
1787
 
@@ -1868,6 +1948,15 @@ body.mobile .preview-panel {
1868
  background: var(--reader-bg);
1869
  }
1870
 
 
 
 
 
 
 
 
 
 
1871
  body.light.mobile .preview-panel {
1872
  --reader-bg: #f7f3e8;
1873
  --reader-header: #f0ecdf;
@@ -1921,7 +2010,7 @@ body.mobile .preview-path {
1921
  z-index: 95;
1922
  width: 85vw !important;
1923
  max-width: 320px;
1924
- transition: transform var(--transition), opacity var(--transition);
1925
  will-change: transform;
1926
  }
1927
 
@@ -2033,26 +2122,11 @@ input[type="checkbox"]:indeterminate {
2033
  accent-color: var(--primary);
2034
  }
2035
 
 
2036
  body.theme-transitioning *,
2037
  body.theme-transitioning *::before,
2038
  body.theme-transitioning *::after {
2039
- transition: background-color 0.4s ease, color 0.4s ease, border-color 0.4s ease, box-shadow 0.4s ease !important;
2040
- }
2041
-
2042
- .theme-ripple {
2043
- position: fixed;
2044
- border-radius: 50%;
2045
- pointer-events: none;
2046
- z-index: 9999;
2047
- width: 0;
2048
- height: 0;
2049
- animation: theme-ripple-expand 0.6s ease-out forwards;
2050
- }
2051
-
2052
- @keyframes theme-ripple-expand {
2053
- 0% { width: 0; height: 0; opacity: 0.6; }
2054
-
2055
- 100% { width: 300vmax; height: 300vmax; opacity: 0; transform: translate(-150vmax, -150vmax); }
2056
  }
2057
 
2058
  @media (prefers-reduced-motion: reduce) {
 
35
  --shadow-sm: 0 1px 2px rgba(0,0,0,0.3);
36
  --shadow-md: 0 2px 8px rgba(0,0,0,0.4);
37
  --shadow-lg: 0 4px 16px rgba(0,0,0,0.5);
38
+ --transition: 160ms cubic-bezier(0.2, 0, 0, 1);
39
+ --motion-standard: 180ms cubic-bezier(0.2, 0, 0, 1);
40
+ --motion-emphasized: 220ms cubic-bezier(0.2, 0, 0, 1);
41
  --sidebar-w: 280px;
42
  --sidebar-expanded-w: min(50vw, 720px);
43
  color-scheme: dark;
 
281
  max-height: 320px;
282
  overflow-y: auto;
283
  padding: 4px 0;
284
+ transform-origin: top center;
285
+ animation: menu-enter var(--motion-standard);
286
+ }
287
+
288
+ @keyframes menu-enter {
289
+ from { opacity: 0; transform: translateY(-4px) scaleY(0.98); }
290
+ to { opacity: 1; transform: translateY(0) scaleY(1); }
291
  }
292
 
293
  .search-history-dropdown .history-item {
 
470
  overflow: hidden;
471
  }
472
 
473
+ .preview-panel.preview-enter {
474
+ animation: preview-enter var(--motion-emphasized);
475
+ }
476
+
477
+ @keyframes preview-enter {
478
+ from { opacity: 0; transform: translateY(6px) scale(0.995); }
479
+ to { opacity: 1; transform: translateY(0) scale(1); }
480
+ }
481
+
482
  body.preview-open {
483
  user-select: none;
484
  -webkit-user-select: none;
 
536
  display: block;
537
  }
538
 
539
+ body.theme-switching .theme-icon-svg {
540
+ animation: theme-icon-turn 280ms cubic-bezier(0.2, 0, 0, 1);
541
+ }
542
+
543
+ @keyframes theme-icon-turn {
544
+ from { opacity: 0.35; transform: rotate(-80deg) scale(0.82); }
545
+ to { opacity: 1; transform: rotate(0) scale(1); }
546
+ }
547
+
548
  .preview-title {
549
  font-size: 15px;
550
  font-weight: 600;
 
634
  }
635
 
636
  .sidebar.collapsed {
637
+ width: 0 !important;
638
  border-right: none;
639
  }
640
 
 
1181
  flex: 1;
1182
  overflow-y: auto;
1183
  overflow-x: hidden;
1184
+ position: relative;
1185
+ }
1186
+
1187
+ .results-container::before {
1188
+ content: "";
1189
+ position: sticky;
1190
+ top: 0;
1191
+ z-index: 6;
1192
+ display: block;
1193
+ width: 100%;
1194
+ height: 2px;
1195
+ margin-bottom: -2px;
1196
+ background: linear-gradient(90deg, transparent, var(--primary), transparent);
1197
+ opacity: 0;
1198
+ transform: translateX(-100%);
1199
+ pointer-events: none;
1200
+ }
1201
+
1202
+ .results-container.is-searching::before {
1203
+ opacity: 1;
1204
+ animation: search-progress 1.15s ease-in-out infinite;
1205
+ }
1206
+
1207
+ .results-container.is-refreshing .results-list {
1208
+ opacity: 0.56;
1209
+ }
1210
+
1211
+ .results-list {
1212
+ transition: opacity var(--motion-standard);
1213
+ }
1214
+
1215
+ @keyframes search-progress {
1216
+ from { transform: translateX(-100%); }
1217
+ to { transform: translateX(100%); }
1218
  }
1219
 
1220
  .snippet-loading {
 
1235
  transition: background var(--transition);
1236
  }
1237
 
1238
+ .result-item.result-enter {
1239
+ animation: result-enter var(--motion-standard) both;
1240
+ animation-delay: var(--result-enter-delay, 0ms);
1241
+ }
1242
+
1243
+ @keyframes result-enter {
1244
+ from { opacity: 0; transform: translateY(4px); }
1245
+ to { opacity: 1; transform: translateY(0); }
1246
+ }
1247
+
1248
  .result-item:nth-child(even) {
1249
  background: var(--surface-variant);
1250
  }
 
1833
  animation: toast-in 0.2s ease;
1834
  }
1835
 
1836
+ .toast.is-leaving {
1837
+ animation: toast-out 140ms ease-in forwards;
1838
+ }
1839
+
1840
  @keyframes toast-in {
1841
  from {
1842
  opacity: 0;
 
1849
  }
1850
  }
1851
 
1852
+ @keyframes toast-out {
1853
+ from { opacity: 1; transform: translateX(-50%) translateY(0); }
1854
+ to { opacity: 0; transform: translateX(-50%) translateY(6px); }
1855
+ }
1856
+
1857
  body.mobile .sidebar {
1858
  position: fixed;
1859
  top: var(--header-h);
 
1861
  z-index: 95;
1862
  width: 85vw !important;
1863
  max-width: 320px;
1864
+ transition: transform var(--motion-emphasized), opacity var(--motion-standard);
1865
  will-change: transform;
1866
  }
1867
 
 
1948
  background: var(--reader-bg);
1949
  }
1950
 
1951
+ body.mobile .preview-panel.preview-enter {
1952
+ animation-name: preview-enter-mobile;
1953
+ }
1954
+
1955
+ @keyframes preview-enter-mobile {
1956
+ from { opacity: 0.7; transform: translateX(18px); }
1957
+ to { opacity: 1; transform: translateX(0); }
1958
+ }
1959
+
1960
  body.light.mobile .preview-panel {
1961
  --reader-bg: #f7f3e8;
1962
  --reader-header: #f0ecdf;
 
2010
  z-index: 95;
2011
  width: 85vw !important;
2012
  max-width: 320px;
2013
+ transition: transform var(--motion-emphasized), opacity var(--motion-standard);
2014
  will-change: transform;
2015
  }
2016
 
 
2122
  accent-color: var(--primary);
2123
  }
2124
 
2125
+ body.theme-transitioning,
2126
  body.theme-transitioning *,
2127
  body.theme-transitioning *::before,
2128
  body.theme-transitioning *::after {
2129
+ transition: background-color 180ms ease, color 180ms ease, border-color 180ms ease, box-shadow 180ms ease !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2130
  }
2131
 
2132
  @media (prefers-reduced-motion: reduce) {