Commit Graph

150 Commits

Author SHA1 Message Date
TerryM
f7c0c0387e fix(stream): preserve FilterChips horizontal scroll across remount
PublicLayout wraps the routed page in <AnimatePresence> keyed by
pathname+search, so changing ?type=… fully unmounts the page and creates
a fresh FilterChips. A useRef-based save/restore therefore reset on
every filter switch. Persist the scrollLeft in a module-level value
that survives the unmount, restore synchronously on mount, and keep an
~1.5s post-mount watch window for the iOS Safari sticky relayout that
asynchronously snaps scrollLeft back to 0. Also gate the inactive-chip
hover color behind [@media(hover:hover)] so iOS sticky-hover no longer
leaves a faint gold tint on the last-tapped filter.
2026-06-03 14:30:27 +08:00
TerryM
985463b7da fix(stream): resolve search deep-links without pagination stall
All checks were successful
Deploy to Frontend Servers / deploy (push) Successful in 37s
Search results can link to older posts that are not present in the first
/browse page. The previous deep-link flow kept paginating the all-assets
stream until the target id appeared, leaving users stuck on the waiting
indicator for very old posts.

Fetch /api/posts/:id directly for ?post= arrivals and inject the resolved
target post at the top of the stream when it is not already in loaded
items. The normal paginated feed still loads below for context. Keep the
explicit finding/not-found status messages as a fallback for slow or
missing direct fetches.

Verified with search result c5eeb17d-3bd0-4d32-9c92-5efa6e4a015c: target
post rendered within 100ms instead of waiting for pagination. Checks:
tsc, format:check, tests, build.
2026-06-03 01:40:21 +08:00
TerryM
edba16bbd2 feat(stream): hold deep-link scroll until first content reveals
All checks were successful
Deploy to Frontend Servers / deploy (push) Successful in 28s
Banner / Home-card deep-links were starting the smooth scroll the
moment the target post entered the DOM, before the in-view Reveal
animations on the top bubbles had time to fade in. Users perceived the
page as 'scrolling past nothing' because most bubbles were still at
opacity 0 when the viewport moved.

Track the moment first non-skeleton content appears for the current
target via firstContentAtRef, then hold the smooth-scroll start until
~300ms after that — long enough for the initial Reveal staggers to
play. Elapsed time is subtracted so cached arrivals don't pay the full
wait twice, and the ref resets per target so each navigation re-times.

Verified in the browser: with cold cache, content arrives ~480ms after
click, smooth scroll starts ~800ms (300ms settle), reaches deep target
by ~1.3s. With warm cache same pattern; users now see content before
motion begins.
2026-06-02 11:48:05 +08:00
TerryM
562843e4b2 feat(stream): reset scroll to top on ?post deep-link arrivals
All checks were successful
Deploy to Frontend Servers / deploy (push) Successful in 28s
Banner / Home-card clicks landing on /browse?post=X now always start the
smooth-scroll positioning from the top of the stream, instead of from
whatever scrollY the user happened to leave the page at. Runs in
useLayoutEffect before paint so the user never briefly sees the previous
position before the jump, giving a clearly visible scroll journey to the
target post every time.

Verified in the browser: before banner click scrollY=2000, immediately
after =0, then smooth-scrolled to ~25k as pagination loaded the target
post deeper in the stream.
2026-06-02 11:42:10 +08:00
TerryM
387b25f1e3 feat(stream): friendlier pagination loading + error retry
All checks were successful
Deploy to Frontend Servers / deploy (push) Successful in 29s
- Replace the bare '…' loading dots at the bottom of the post stream
  with a skeleton bubble that matches the initial-load placeholders, so
  pagination feels like content arriving instead of a frozen indicator.
- Localize the retry control via new 'retry' / 'loadMoreFailed' keys
  across all 7 locales and surface a user-friendly error string instead
  of the raw exception message.
- Retry button now picks reset() vs loadMore() based on item count so a
  pagination failure only refetches the next page, not the whole stream.
- When a banner deep-link can't find its target post and pagination
  errors, break out of the retry loop and release the scroll lock so the
  user sees the inline retry instead of an endless freeze.

Verified in the browser: zh-CN renders '加载更多资料失败,请检查网络后重试。'
with a '重试' button; banner clicks with empty / '#' / 'javascript:' /
null linkUrls render no anchor and do not navigate.
2026-06-02 11:39:17 +08:00
TerryM
7ed9f8c8bf perf(banner): smoother deep-link from banner to /browse post
All checks were successful
Deploy to Frontend Servers / deploy (push) Successful in 27s
- MessageStream: drop the mount-time scroll lock and the 80ms-delayed
  custom rAF; engage the lock only while the smooth animation runs and
  use native scrollTo({behavior:'smooth'}) so the page never feels frozen
  during pagination and the easing is buttery.
- PublicLayout: fire the default /browse prefetch immediately on mount
  (banner / Home tile destination) so a fast tap hits a warm cache;
  popular / latest stay deferred to idle.
- FigmaBanner: prefetch the all-scope stream on mount and on pointerdown
  as safety nets, and ignore empty / '#' / javascript: link URLs so a
  contentless banner renders as a non-interactive image.
- usePostStream: dedupe in-flight prefetches by key so concurrent
  callers (layout + banner) collapse into a single network request.
2026-06-02 11:30:47 +08:00
TerryM
fbb9d21f24 fix: cap banners shown to 10 with a single-row dot indicator
All checks were successful
Deploy to Frontend Servers / deploy (push) Successful in 28s
The banners API can return hundreds of records; show at most 10 so the carousel
and its dot indicator stay on one row within the phone width, regardless of how
many exist on the backend.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 11:16:49 +08:00
TerryM
e752de67e1 fix(banner): preserve active language when navigating to post links
Banner linkUrls come back from the API as unprefixed paths
(e.g. /browse?post=123). Navigating to them directly dropped non-English
viewers into the English version of the post. Localize both the rendered
href and the SPA navigate target via stripLangPrefix + localizePath.
2026-06-02 11:12:26 +08:00
TerryM
d3e562663d fix: cap banner dot indicator at 10 to avoid mobile overflow
All checks were successful
Deploy to Frontend Servers / deploy (push) Successful in 40s
With many banners the pagination dots overflowed the phone width. Show at most
10 dots in a window that follows the active slide; each dot still maps to its
real slide index.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 11:01:15 +08:00
TerryM
92210cf0a2 fix(lightbox): extend save hint display to 2.5s 2026-06-02 10:52:08 +08:00
TerryM
6c0c3b89a9 fix(lightbox): anchor save hint below rendered image
Measure the image's rendered bottom edge with refs + ResizeObserver and
position the long-press save hint relative to it instead of pinning to
screen center or stage bottom. Enlarges the toast for mobile legibility
and clamps the offset so tall portrait images don't push it offscreen.
2026-06-02 10:51:17 +08:00
TerryM
8acb3a281b fix: opt out of browser auto-translation
The app ships its own 7-language i18n and serves localized content, but mobile
browsers (Google Translate) were auto-translating the Chinese UI into broken
English (brand, nav, language dropdown). Add translate="no" + the Google
notranslate meta, and keep the attribute set on language changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 10:47:36 +08:00
TerryM
5a5acfcbc2 fix: show save guide on mobile only
All checks were successful
Deploy to Frontend Servers / deploy (push) Successful in 27s
2026-06-01 23:13:25 +08:00
TerryM
e096d59fa6 feat: add media save guide 2026-06-01 23:00:28 +08:00
TerryM
7b48f9780c fix: use backend video preview urls
All checks were successful
Deploy to Frontend Servers / deploy (push) Successful in 37s
2026-06-01 17:52:33 +08:00
TerryM
a968f47640 feat: support mobile video previews 2026-06-01 16:35:40 +08:00
TerryM
337d19e626 feat(i18n): split locale dicts into src/locales/ and add full Korean translation
- Extract zhDict/enDict from i18n.tsx into src/locales/{zh-CN,en}.ts
- Add full Korean dictionary (src/locales/ko.ts) covering all 115 UI keys
- Update formatBytes test/impl boundary for 1000-based units
2026-06-01 15:49:15 +08:00
TerryM
c32ae539f6 fix: use decimal (1000-based) units in formatBytes to match S3/curl display 2026-06-01 15:24:41 +08:00
TerryM
fa78568c94 feat: add localized home routes 2026-06-01 15:09:58 +08:00
TerryM
186ba362f3 fix: align banner width with 3-column latest section 2026-05-31 18:40:27 +08:00
TerryM
06fe117ebc feat: render desktop latest section as 3-column masonry
- Matches Figma design (file uHDZkVHjAp7BXDKQKB0PM4, node 4367-11405).
- Mobile keeps the existing 5-post single column unchanged.
- Desktop (md+) renders all 12 latest posts in a CSS-columns masonry
  with break-inside-avoid so each card's height stays content-driven.
- Adds an optional 'fluid' prop to MessageBubble that drops the
  standalone-feed max-widths so bubbles fill the masonry column. The
  /browse stream keeps the default non-fluid widths.
2026-05-31 18:35:57 +08:00
TerryM
c7e0562d9a feat: desktop banner peek with framer-motion blur
Match Figma node 4366-11092 desktop banner design:
- Slides shrink to 78%/72%/60% width on md/lg/xl with snap-center,
  first/last get matching left/right margin so the edges still center.
- Each slide is wrapped in a framer-motion m.div that animates filter,
  opacity, and scale between active and idle states.
- goTo and scroll/drag handlers use the slide's real offsetWidth so
  centering math holds at every breakpoint; mobile (full-width, snap-start
  visual) is unchanged.
2026-05-31 18:22:03 +08:00
TerryM
c71ebba807 fix: simplify desktop search panel
All checks were successful
Deploy to Frontend Servers / deploy (push) Successful in 25s
2026-05-31 03:21:23 +08:00
TerryM
345ccb0a25 fix: preview search results 2026-05-31 03:10:56 +08:00
TerryM
6b3211f26f style: format homepage files
All checks were successful
Deploy to Frontend Servers / deploy (push) Successful in 34s
2026-05-31 02:56:34 +08:00
TerryM
39c593c454 fix: add desktop search dropdown 2026-05-31 02:55:04 +08:00
TerryM
cf6bd7339e fix: unify homepage card background with the browse page
Match the official-recommendation and popular cards to the message bubble
surface color (#272632) used on the browse/all page and the latest section,
so homepage content cards and the browse page share one background. Also
align the popular card border with the official card (#27292E).

Categories tiles are intentionally left unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 02:48:18 +08:00
TerryM
46b7ee861e fix: expire frontend caches 2026-05-31 02:44:44 +08:00
TerryM
5b93e8dc77 fix: match banner and popular list width to the latest cards
Size the banner and the popular rank list to the same responsive widths as
the latest section's message bubbles (680/900/1120), so the banner, latest
cards and popular cards line up at one consistent content width on desktop.

Desktop-scoped; mobile stays full-width.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 02:39:54 +08:00
TerryM
e35573083a fix: align homepage section titles and unify download button color
- Wrap the Categories and Popular sections in the same responsive
  max-width container (820/1080/1180) used by Official and Latest, so all
  four section titles line up vertically on desktop.
- Official carousel arrows: hide the arrow at the edge already reached, and
  snap one card per click (reveal the next/previous card fully, keeping a
  small peek) instead of a fixed-pixel scroll.
- Show the pagination dots on desktop too (was mobile-only).
- RecommendedCard download button icon: text-ark-gold -> text-white to match
  the file bubble / popular / video download buttons.

Desktop-scoped; mobile layout unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 02:32:15 +08:00
TerryM
320e09cc87 fix: desktop homepage responsive polish and nav tweaks
- Banner: scale down and center on desktop (md:max-w-[680px] lg:max-w-[800px])
  so it no longer fills the whole screen.
- Align section widths to one responsive container (820/1080/1180): wrap the
  Official row and match Popular to Latest, fixing left-edge/width mismatches.
- RecommendedCard: stop the carousel card from shrinking back to 246.4px at xl.
- Popular download button now matches the file bubble's filled round
  DownloadCloud button for visual consistency.
- Header nav vertical padding py-1 -> py-0.5; swap Favorites before Popular
  across desktop nav and mobile menu to match the bottom tab order.
- Official carousel: hide the left arrow at the start and the right arrow at
  the end instead of always showing both.

All changes are md/lg/xl-scoped; mobile layout is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 02:04:26 +08:00
TerryM
9ac072e8d8 fix: jump to top on page change before post deep-link alignment
All checks were successful
Deploy to Frontend Servers / deploy (push) Successful in 26s
Entering /browse via a popular-section card carried ?post=, which made
ScrollToTop skip the reset entirely. The window stayed at the previous
page's bottom scroll, so the deep-link animation visibly scrolled UP from
the bottom to the target post.

Now any pathname change jumps to the top first (even with ?post=), letting
the destination align to the post by scrolling down from the top. Hash
anchors and same-page ?post= changes are still left alone.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 23:47:26 +08:00
TerryM
5179a8c068 Fix popular rank dates 2026-05-30 23:33:23 +08:00
TerryM
7ed4cbbeba fix: disable video controls text selection 2026-05-30 22:19:20 +08:00
TerryM
cc58ee8aac fix: smooth mobile footer tab switching 2026-05-30 21:48:14 +08:00
TerryM
d531ba40f3 fix: tighten mobile bottom nav spacing 2026-05-30 21:30:08 +08:00
TerryM
5277943196 fix: anchor mobile bottom nav 2026-05-30 21:27:48 +08:00
TerryM
5ce52943e9 feat: add image save hint 2026-05-30 18:44:15 +08:00
TerryM
942db88f58 fix(ui): disable text selection on header, filter chips, and bottom nav
Add `select-none` to the sticky header, the type filter chips row, and the
mobile bottom nav so their labels and icons can't be highlighted/selected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 18:14:58 +08:00
TerryM
b2e4a4e710 fix: bound post deeplink scrolling 2026-05-30 18:08:39 +08:00
TerryM
41299b5b65 feat(deeplink): jump from banner/rank list to the exact post in All Materials
- FigmaBanner: route same-app linkUrl through SPA navigation so the stream's
  scroll-to-post runs without a full reload; defer pointer capture until a real
  drag starts, fixing plain clicks being swallowed by setPointerCapture
- PopularRankList: rank rows navigate straight to /browse?sort=popular&post=<id>
- MessageStream: ?post= deep links jump directly to the target instead of
  resetting to the top and animating through the stream
- ScrollToTop: skip the top-reset for ?post= navigations so the target page
  handles its own alignment

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 17:54:16 +08:00
TerryM
0733ea8b18 feat(category-icon): 分类图标支持 PNG 资源,学院类目改用 PNG
categorySvgUrlForSlug → categoryAssetUrlForSlug,映射值改为带子目录
的相对路径(svg/ 或 png/),新增 academy-materials / academy-video
的 PNG 图标并兼容拼写别名 acedemy-video。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 17:34:18 +08:00
TerryM
07f040a549 fix(video): 全屏关闭后内嵌进度条跳变而非从0扫动
程序化 seek 同步全屏播放进度时,新增 snapProgress 抑制进度条
的宽度过渡动画,使其直接落到观看位置,恢复播放时再启用过渡。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 16:54:03 +08:00
TerryM
40d64f1293 fix: 链接预览强调色按域名兜底,腾讯会议不再用黑色
部分站点(如 meeting.tencent.com)返回 themeColor=#000000,在深色气泡上看不见。
按域名覆盖为品牌金色,其余仍优先用 themeColor。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 15:43:30 +08:00
TerryM
9bef178bc8 feat: 大图查看器支持 iOS 长按"存储到照片"
去掉全尺寸图上的 select-none 并显式设 -webkit-touch-callout:default,使 iOS
Safari 长按图片能弹出原生「存储到照片」菜单(保存的是 current.url 全图)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 15:43:30 +08:00
TerryM
cc9f0a5730 fix(stream): preserve filename extension in narrow bubbles
Previously the file bubble wrapped JS middle-ellipsis in CSS truncate,
so narrow containers silently clipped the tail+extension, leaving
e.g. "25cb264a-e06…." instead of "25cb264a-e06…811a.jpg".

Split the displayed name into a shrinking head (with CSS truncate)
and a non-shrinking tail (last 4 base chars + extension). The browser
now decides how much head to clip while the suffix is always visible.
2026-05-30 15:42:13 +08:00
TerryM
a8fd540ef5 fix(stream): smooth scroll to linked post cards
Route resource card clicks to /browse?post=<id> instead of relying on hash
anchors, then manually calculate the sticky filter offset when scrolling to
the target bubble. Start from the top and smooth-scroll to the card for a
clear transition, with delayed auto realignments after media above the target
settles.
2026-05-30 03:11:03 +08:00
TerryM
6798e90708 feat(posts): support short titles for resource cards
Add optional post-level and localized title fields, and use a new
postTitleText helper for Resource.title so card/list surfaces prefer short
backend-provided titles instead of full body text. When title is missing,
fallback to the first non-empty body line, then filename, then post id.

Document the backend handoff in docs/posts-title-api.md alongside the other
backend task docs.
2026-05-30 03:11:03 +08:00
TerryM
4c441244c8 style: 搜索提示行与输入框内容像素级对齐
输入框 border 改为 ring-inset(不占布局),使输入框内容与下方提示行(ⓘ+文字)
都位于「容器左缘+12px」同一基准,图标列严丝合缝对齐,不再依赖魔法数值。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 03:00:44 +08:00
TerryM
ea38503f37 ui(thumbnails): force preview images to fixed ratio
Use object-fill for compact preview thumbnails so rank-list covers and
file previews fill their fixed boxes without cropping or black bars. This
keeps the list layout stable while matching the desired compressed-ratio
thumbnail treatment.
2026-05-30 02:51:09 +08:00