632 Commits

Author SHA1 Message Date
xfy
c0af99736d docs: update AGENTS.md for vite 8 build, vitest tests, and tiptap toolchain 2026-06-24 10:12:16 +08:00
xfy
08c2539dfc test(tiptap): add isValidUrl tests covering scheme validation edge cases 2026-06-24 10:10:39 +08:00
xfy
90cff2d0cc test(tiptap): add UploadImageNodeView tests for rendering/update/callbacks/destroy 2026-06-24 10:09:36 +08:00
xfy
f7042d3ab4 test(tiptap): add UploadCoordinator tests for counts/lifecycle/emit/idempotency 2026-06-24 10:07:43 +08:00
xfy
8ff051689f refactor(tiptap): export isValidUrl and UploadImageNodeView for testability 2026-06-24 10:04:59 +08:00
xfy
19532670d3 build(tiptap): add vitest config with happy-dom environment 2026-06-24 10:04:23 +08:00
xfy
21c0359f5e build(tiptap): upgrade to vite 8/vitest 4/typescript 6/tiptap 3.27, migrate to rolldownOptions 2026-06-24 10:03:47 +08:00
xfy
939e0de171 build(tiptap): run tsc --noEmit before vite build 2026-06-23 18:45:17 +08:00
xfy
6ffcd55c94 fix(tiptap): correct link command order + validate URL scheme in slash commands 2026-06-23 18:42:57 +08:00
xfy
e580c3ab18 refactor(tiptap): narrow updateNodeAttrs type, extract findImageNodeByUploadId helper 2026-06-23 18:42:05 +08:00
xfy
d099db4140 refactor(tiptap): move coordinator to editor.storage, drop dead Markdown html option 2026-06-23 18:39:38 +08:00
xfy
6b44a9c036 fix(tiptap): clean up pending upload on NodeView.destroy to prevent blob leak 2026-06-23 18:36:47 +08:00
xfy
2e239908a4 fix(tiptap): revoke blob on node destroy + maintain upload counts internally 2026-06-23 18:36:07 +08:00
xfy
71df6abe5d refactor(tiptap): drop redundant single-arg mergeAttributes call 2026-06-23 18:35:05 +08:00
xfy
18ae6a915e refactor(tiptap): type coordinatorRef via Pick<UploadCoordinator> instead of duck typing 2026-06-23 18:34:37 +08:00
xfy
558191e922 refactor(tiptap): type CommandItem.editor as Editor, explicit SlashPopup return type 2026-06-23 18:33:59 +08:00
xfy
073f6936e3 build(tiptap): add tsconfig + typecheck script 2026-06-23 18:32:26 +08:00
xfy
c1610ace99 fix(lightbox): prevent background flash on open
Some checks failed
CI / check (push) Failing after 5m16s
CI / build (push) Has been skipped
overlay inherited opacity:1 from .lightbox-overlay CSS, so the moment
it was appended to the DOM it painted a full-opacity black background.
this showed for ~24ms (the time until the image loaded and start() ran
the first frame that sets opacity 0), visible as a black flash before
the fade-in.

set overlay.style.opacity = '0' right after creating it, so it is
transparent when appended; start()'s transition then fades it to 1 as
intended.
2026-06-23 17:37:38 +08:00
xfy
8bdc3553bd fix(lightbox): correct zoom origin, scroll-close, blur-up rendering
several bugs found via in-browser debugging, all in lightbox.js + the
blur-up CSS:

1. DOMRect attribute mismatch: getBoundingClientRect returns .width/
   .height, but code read .w/.h -> NaN transforms. map rectOf() to a
   uniform {x,y,w,h}.

2. zoom scale base was the huge original natural size (2942px), so the
   centered state collapsed to a tiny scale and the open animation ran
   backwards (shrink). base the scale on originRect instead: first
   frame is the in-article image at scale 1, centered state scales to
   target.w/originRect.w. open is always grow, close always shrink.

3. scroll-to-close fired on a phantom scroll jump: the oversized
   original img (layout size = naturalW) expanded the document's
   scrollable area. set img layout size explicitly and add
   overflow:hidden on the (position:fixed) overlay so the img cannot
   expand the body.

4. first open animation jumped from the wrong position: a single rAF
   was not enough to commit the no-transition first frame. use a forced
   reflow (offsetHeight) + double-rAF before starting the transition.

5. blur-up full layer stuck at opacity 0 until a forced reflow (opening
   the lightbox) repainted it. drop the opacity transition on .blur-img-
   full and set opacity:1 from JS on load (deterministic, no repaint
   race). mark the container .is-loaded and hide the placeholder via
   the container class.

6. a 16px grey gap above in-article images: .md-content img { margin:
   1rem 0 } applied to the absolute .blur-img-full too, pushing it
   down. reset margin:0 / max-width:none on the blur-up img layers.
2026-06-23 17:30:37 +08:00
xfy
940236d84f style: unify lightbox CSS, adapt entry-cover and post-card blur
remove the duplicated .image-viewer-* and .md-image-lightbox-* rules
(~93 lines); add a single .lightbox-* set sized/positioned by JS
transform. entry-cover and post-card-cover now target the blur-img
structure (4:3 ratio on cards, cover-fill on entry cover).
2026-06-23 15:53:14 +08:00
xfy
cb8c42e833 refactor: remove obsolete ImageViewer component
both call sites (PostCover, PostCard) now render raw .blur-img, so the
Dioxus ImageViewer component — with its leaky closure.forget() and dead
js_sys::eval delay — is no longer needed. remove the file and its mod
declaration.
2026-06-23 15:51:40 +08:00
xfy
8b1d55613e refactor(post-card): use raw blur-img for cover, drop ImageViewer
card cover is display-only (click goes to the card link), so it uses
a bare .blur-img with ?w=20 placeholder + ?thumb=400x300 display, no
lightbox-single class. card pages are outside .post-content/.entry-cover
so lightbox.js never scans them.
2026-06-23 15:50:29 +08:00
xfy
75b9080df8 refactor(post-cover): use raw blur-img structure as single lightbox image
drop ImageViewer; render .blur-img with a lightbox-single class (the
class, not a data attribute, because Dioxus' typed span elements reject
arbitrary data-* attrs). lightbox.js now keys single-mode off the
lightbox-single class instead of data-single. cover uses ?w=20
placeholder + ?w=1200 display; SSR writes --ar from real dimensions.
2026-06-23 15:48:02 +08:00
xfy
f88d83b0ad feat(post-content): load lightbox.js and init gallery
eval both post-content.js (copy) and lightbox.js (lightbox + lazy-load),
then call __initLightbox over ['.post-content', '.entry-cover'] so both
in-content images (gallery) and the cover (single) are wired up.
2026-06-23 15:45:30 +08:00
xfy
bace31933f refactor(post-content): remove lightbox/lazy-load, keep copy only
the lightbox + IntersectionObserver lazy-load now live in lightbox.js;
post-content.js is reduced to just the code-block copy button.
2026-06-23 15:44:46 +08:00
xfy
5d2b6e502d feat(lightbox): gallery navigation with fade, arrows + keyboard
add prev/next arrow buttons (only in gallery mode with >1 image) and
ArrowLeft/ArrowRight keyboard handling. switching uses a 150ms
fade-out -> swap src -> fade-in (no fly, matching Medium). originNode
is updated to the new image so close/scroll-close flies back to the
right place; openScrollY is reset to keep scroll-close baseline valid.
2026-06-23 15:43:24 +08:00
xfy
ab4f006107 fix(lightbox): bind interactions immediately, before image loads
bindInteractions() was only called from start(), which waits for the
image load event. during the load window Esc/scroll/backdrop-click did
nothing. bind right after appending the overlay so the lightbox is
closable throughout the load.
2026-06-23 15:41:22 +08:00
xfy
a5c7e64230 feat(lightbox): open/close with origin-aware zoom + scroll-to-close
click a .blur-img to open: record its viewport rect, the lightbox img
starts at that position/size and transforms to centered (Medium-style
origin-aware zoom). close flies back to the live originNode rect.

scroll-to-close: while open, any window scroll drives the image back
to its (moving) position via per-frame rect tracking + linear
interpolation; the article keeps its scroll position on close.

Esc + click-on-backdrop close; click-on-image does not close.
prefers-reduced-motion falls back to fade-only / immediate close.
2026-06-23 15:29:59 +08:00
xfy
fe58d25b54 feat(lightbox): add lightbox.js skeleton with lazy-load 2026-06-23 15:28:47 +08:00
xfy
e68b77aac0 style(highlight): unify code block background via --color-paper-code-block
strip syntect-injected background-color from generated CSS so the code
block background comes from a single source (--color-paper-code-block
in input.css), avoiding double-layer color mismatch at the .code span
edge. update the variable values for both light and dark themes.
2026-06-23 15:28:12 +08:00
xfy
975e331dd5 fix(write): split tags on fullwidth punctuation
The tag splitter matched ASCII ',' and ';' twice instead of the
fullwidth ',' (U+FF0C) / ';' (U+FF1B) the comment intended, so tags
separated by fullwidth punctuation (common under Chinese IMEs)
collapsed into a single tag.

Also resolves the compiler warnings surfaced in the same build:
- tiptap_bridge: drop unused Readable/Writable imports and
  EditorInstance/TiptapEditorModule re-exports
- tiptap_bridge: migrate deprecated RequestInit::method/body/
  credentials builders to set_method/set_body/set_credentials
- write: remove redundant `mut` on Dioxus signals that are only
  read/set, never reassigned
- image_viewer: gate the server-only `mut` reassignment so the WASM
  build (which strips the cfg block) doesn't warn
2026-06-23 14:44:18 +08:00
xfy
ef6a3f0da2 fix(home,editor): repair post card nested anchors and editor upload state
首页审查修复(home/post_card/image_viewer/search/home_skeleton):
- PostCard 消除嵌套 <a>:外层 Link 包裹改为绝对定位覆盖层链接,
  标题/摘要/封面降级为普通元素,标签用 z-10 叠在覆盖层之上并
  stop_propagation,整卡点击跳转文章详情。
- ImageViewer 新增 lightbox prop(默认 true),卡片封面传 false
  禁用灯箱以归一为单一跳转交互;文章详情页零改动。
- home.rs 越界页码守卫:分页仅在 total>0 渲染,避免 /page/9999
  出现孤立空分页;错误文案脱敏为"加载失败"。
- search.rs 错误文案脱敏为"搜索失败",与首页/标签页一致。
- HomeSkeleton 卡片 5→10,对齐 POSTS_PER_PAGE。

编辑器上传重构(write/tiptap_bridge):
- consume_upload_event 改用 upload_errors Vec 自身判重,移除
  seen_error_ids 副本状态;Signal 参数加 mut 修复 E0596 编译错误。
- make_upload_closure 构造阶段错误以 rejected Promise 返回而非
  panic,单张坏文件不再拖垮整个编辑器。
- write.rs 移除未用的 success signal,编辑保存后统一跳转 Posts,
  标签分隔符支持半角/全角逗号与分号,提取 META_*_CLASS 常量去重。
- Makefile dev 目标清理 static/ 目录。
2026-06-23 14:14:28 +08:00
xfy
1e270f0055 refactor(tiptap): replace js_sys::eval with wasm-bindgen bindings
Some checks failed
CI / check (push) Failing after 5m21s
CI / build (push) Has been skipped
Tiptap 编辑器与 Rust/WASM 宿主的通信层从 eval 字符串拼贴 + window 全局 + 轮询,
重构为类型化 wasm-bindgen 绑定:

- 新增 src/tiptap_bridge.rs:extern 绑定 TiptapEditor/EditorInstance/EditorOptions/
  UploadEventJs;EditorHandle 统一持有实例 + 4 个 Closure,Drop 调 destroy 杜绝泄漏。
- write.rs 删除全部 9 处 js_sys::eval、两段轮询(100ms 就绪 / 500ms 上传事件)、
  三个 window 全局(__tiptap_ready/__tiptap_content/__tiptap_uploads)。
- JS 侧 EditorOptions 加 onReady/onUploadEvent 回调;UploadCoordinator.notifyRust
  改为 emit 回调(事件携带 counts),不再写 window 全局。
- make_upload_closure 用 web_sys fetch 实现 /api/upload,行为与原 JS 一致。

净变化:354 insertions(+), 496 deletions(-)。

Design: docs/superpowers/specs/2026-06-23-tiptap-wasm-bindgen-refactor-design.md
Plan: docs/superpowers/plans/2026-06-23-tiptap-wasm-bindgen-refactor.md
2026-06-23 13:40:22 +08:00
xfy
a6d30ed5d9 refactor(write): address code review feedback
- remove duplicate `use dioxus::prelude::*` (keep #[allow] inline with comment)
- update write_editor doc comment: callback-driven init, EditorHandle::drop cleanup
  (no more polling/globals)
- restore empty-url check in make_upload_closure (match original JS `success && url`):
  empty url on success now rejects with clear message instead of inserting broken image
- update AGENTS.md tiptap section: write.rs uses tiptap_bridge, no eval/polling
2026-06-23 13:32:34 +08:00
xfy
a3896c24a5 refactor(write): replace all js_sys::eval with tiptap_bridge bindings
write.rs changes (Tasks 7-11):
- use_drop: EditorHandle::drop replaces eval destroy + global reset (#1)
- init use_effect: closure-driven bridge.create replaces eval initEditor
  script + 100ms ready polling (#2/#3/#4/#5)
- getMarkdown: read via EditorHandle instead of eval (#8)
- delete upload polling use_future: driven by onUploadEvent closure (#6/#7)
- removeUploadByUploadId: call via EditorHandle instead of eval (#9)

Bridge fixes during integration:
- UploadCountsJs: drop Copy derive (wasm_bindgen types can't be Copy)
- split module: shared types compile on both targets, wasm externs gated
- add WritableExt/Readable/Writable trait imports for .write()/.set()
- use FnMut closures (Signal write/set take &mut self)
- consume_upload_event takes &mut Signal params

Also: remove stale window.__tiptap_content in index.ts source-mode handler.
2026-06-23 11:25:06 +08:00
xfy
84eea3272a feat(bridge): wasm-bindgen layer for TiptapEditor
Adds src/tiptap_bridge.rs with extern bindings for window.TiptapEditor
module, EditorInstance, EditorOptions (builder), UploadEventJs.
EditorHandle unifies instance + 4 closures lifecycle (Drop calls destroy).
consume_upload_event replaces the old polling body. make_upload_closure
does the /api/upload fetch in Rust (web_sys) instead of eval'd JS.
2026-06-23 10:56:00 +08:00
xfy
9ef3fbfed9 build: add web-sys features for tiptap bridge upload closure 2026-06-23 10:52:59 +08:00
xfy
92cfda7d5a refactor(tiptap): EditorOptions onReady/onUploadEvent callbacks replace polling 2026-06-23 10:52:30 +08:00
xfy
b09216ef29 refactor(tiptap): UploadCoordinator emit callback replaces window global 2026-06-23 10:52:20 +08:00
xfy
9c2b0b1a66 test(markdown): remove diagnostic marker, keep slash verification test
移除 data-pipeline-v2 诊断标记(已完成使命:确认新二进制含新代码)。
保留 full_pipeline_wrap_then_clean_preserves_slash 测试,它验证完整
渲染管线(wrap → clean_html)的斜杠格式不被 sanitizer 破坏。

根因记录: 文章正文 HTML 存在 posts.content_html 字段,创建/更新时
渲染一次存入 DB,展示时直接读 DB 不重新渲染。blur-up 的 markdown
改动只对新渲染生效,存量文章需重新保存或 rebuild_content_html 触发重渲染。
2026-06-23 09:58:53 +08:00
xfy
ad0d884065 fix(makefile): disable SSR cache in dev to avoid stale rendered HTML
dx serve 热重载(重新编译代码)不会清除进程内的 IncrementalRenderer
缓存(DashMap<Uri, CachedHTML> in-process)。改了 SSR 渲染逻辑(如
markdown 渲染、组件结构)后,即使二进制重新编译了,dev server 仍返回
进程启动时缓存的旧 HTML,直到 TTL(默认 3600s)过期。

坑: Ctrl+C 重启 make dev 时,如果 dx serve 子进程没被真正杀掉(进程
PID 不变),内存缓存持续存在。表现为"代码改了但页面不变"。

修复: dev target 给 dx serve 加 SSR_CACHE_SECS=0,禁用 SSR 缓存。
每次请求都重新渲染,改代码立即生效。仅影响 dev,不影响生产构建。
2026-06-23 09:33:13 +08:00
xfy
d194f17013 test(markdown): verify aspect-ratio uses slash separator with real dimensions 2026-06-22 18:24:29 +08:00
xfy
6e98c2abfa fix(blur-up): use slash separator for aspect-ratio (--ar W / H)
根因: CSS aspect-ratio 合法语法是 "width / height"(斜杠分隔),
但 --ar 生成时用了冒号 ":"(--ar:2974:2066)。冒号在 CSS 里是声明分隔符,
2974:2066 被判非法值, aspect-ratio 退化为 auto, 容器高度为 0, 图片不可见.

控制台数据证实: aspectRatio:"auto", clientHeight:0, arVar:"2974:2066"

修复: markdown.rs(正文图) 和 image_viewer.rs(封面) 两处把
{}:{} 改成 {} / {}. CSS 规则 aspect-ratio: var(--ar) 不变,
var 展开后变成合法的 "2974 / 2066".
2026-06-22 18:11:04 +08:00
xfy
0413e539fe docs(env): document IMAGE_DIMENSIONS_CACHE_TTL_SECS 2026-06-22 18:01:31 +08:00
xfy
8ef9ee5621 feat(post-content): lazy-load hi-res images with blur-up fade-in 2026-06-22 18:00:52 +08:00
xfy
1a41f8d7aa feat(image-viewer): render blur-up double-layer structure 2026-06-22 18:00:13 +08:00
xfy
ed91590978 style: add blur-up progressive image loading styles 2026-06-22 17:59:30 +08:00
xfy
0fc56adfcd feat(markdown): wrap uploads images with blur-up double-layer structure 2026-06-22 17:58:37 +08:00
xfy
4690b64a99 feat(sanitizer): allow data-src/class/style on img for blur-up 2026-06-22 17:57:05 +08:00
xfy
45d14e2ad9 feat(image): add image dimensions cache with header-only reading 2026-06-22 17:55:16 +08:00