1013 Commits

Author SHA1 Message Date
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
xfy
6e2e261156 docs(plan): blur-up progressive image loading implementation plan
8 个任务的实现计划,对应 blur-up spec:
- Task 1: image.rs dimensions cache(moka sync + 读 header,webp 走 zenwebp)
- Task 2: sanitizer 扩展放行 data-src/class/style(仅文章)
- Task 3: markdown.rs img 包装成双层 wrapper(正则后处理)
- Task 4: input.css blur-up 样式(模糊占位 + opacity 淡入)
- Task 5: image_viewer.rs 双层结构(SSR 读 dimensions)
- Task 6: post-content.js IntersectionObserver 懒加载 + onload 淡入
- Task 7: .env.example IMAGE_DIMENSIONS_CACHE_TTL_SECS
- Task 8: 全量验证(cargo test/clippy/dx check/css + 手动清单)

每个任务含完整代码、行号、测试、commit。
2026-06-22 17:47:50 +08:00
xfy
987a0d9671 docs(spec): blur-up progressive image loading design
文章图片 blur-up 渐进加载设计文档,经 brainstorming 流程产出:

- 低分辨率占位图(?w=20,SSR 内嵌)+ 高清缩略图双层叠加
- 服务端读图片 header 拿真实尺寸生成 aspect-ratio(零 CLS)
- dimensions 用 moka sync cache 缓存(TTL 可配置,默认 24h)
- 三处图片位置统一(卡片封面/详情封面/正文图)
- Markdown 渲染器把 img 转双层 wrapper,post-content.js 懒加载高清
- sanitizer 扩展放行 data-src/class/style(仅文章,评论不动)
- 外链图不处理,JS 禁用时渐进降级到模糊占位图

webp 尺寸读取风险已排除(zenwebp info() 只读 header)。
验收标准 12 条。不含实现,仅设计。
2026-06-22 17:40:08 +08:00
xfy
876cb55e91 style(editor): smooth blur/grayscale transition on placeholder image
给 img 加 transition: filter 0.2s ease,blur 出现和消失(上传中→成功/
失败)平滑过渡,而非瞬间切换。transition 写在基础 img 规则上,
确保 is-uploading class 移除时过渡也能触发。
2026-06-22 16:27:02 +08:00
xfy
6e04940edc style(editor): blur uploading placeholder image, keep overlay text sharp
上传中占位符图片加高斯模糊,遮罩文字保持清晰。

- .is-uploading img 加 filter: blur(8px)
- container.is-uploading 加 overflow: hidden 防止 blur 边缘渗出
- overlay 文字(spinner/"上传中…")不受影响:overlay 是 img 的兄弟元素
  而非后代,CSS filter 只作用于目标元素本身,不波及兄弟
2026-06-22 16:24:02 +08:00
xfy
75adaa2e1f fix(write): use signal for seen_error_ids to satisfy FnMut constraint
dev server (wasm32 target) 暴露的编译错误:
use_future 的 FnMut 闭包要求可重复调用, 但 seen_error_ids 作为局部
HashSet 被 move 进 async 后无法多次消费 (E0507).

改为 use_signal(HashSet::new): signal 是 Copy, 可被 FnMut 多次 move
进 async, 用 .write()/.read() 访问.

教训: cargo check (默认 target) 不编译 #[cfg(target_arch="wasm32")]
代码, 这类改动必须用 dx check 验证. 之前 Task 13 用 cargo check 漏判了.

验证: dx check (wasm32, 通过) + cargo test (384 passed)
2026-06-22 15:55:10 +08:00
xfy
cee4f3f3ef fix(upload): address code review findings
final code review 后的修复:

1. blob 检查收紧(Important): md.contains("blob:") 误伤合法讨论 blob URL
   的代码块/正文,改为检测 markdown 图片语法 ](blob: 才算占位符泄漏
2. 重试错误原地更新(Minor): seen_error_ids 去重逻辑导致重试后再失败时
   顶部提示停留在旧错误文案;改为已存在 id 时原地更新 message
3. destroy 清理 coordinatorRef(Minor): destroy() 调 setUploadCoordinator(null),
   避免 NodeView 指向已销毁的 coordinator(防御性)
4. 删除 NodeView 未用的 editor 字段(Nit): 死代码清理

验证: cargo test (384 passed) / clippy (本次零警告) / dx check / tiptap build 全过
2026-06-22 15:49:23 +08:00
xfy
a10f79d47e feat(write): upload failure notices, save blocking, and server error passthrough
- 顶部上传失败提示:多条堆叠,×关闭同时删除编辑器内失败占位符(避免孤儿)
  for err in upload_errors().clone() 取 owned 值,onclick 捕获 owned id
- 保存拦截双重防护:
  · counts 检查(uploading/error > 0 阻止保存并提示张数)
  · markdown 兜底扫描 blob:/data-upload-state(轮询窗口期漏判防护)
- onImageUpload fetch 改造:非 2xx 时读取服务端 error 字段,
  透传中文错误(如"文件超过大小限制")而非 'Upload failed: 413'
2026-06-22 15:38:53 +08:00
xfy
d90dd60e89 feat(write): poll window.__tiptap_uploads for upload events and counts 2026-06-22 15:35:17 +08:00
xfy
cb14924de8 feat(write): add upload state signals for placeholder tracking 2026-06-22 15:33:43 +08:00
xfy
8ffb4451b1 style(editor): add upload placeholder NodeView styles (overlay/spinner/error) 2026-06-22 15:32:32 +08:00
xfy
34c8b47436 feat(editor): route slash-command image upload through coordinator
- slash-command.ts: SlashCommandOptions 加 onInsertUploading option
- slash-command.ts: 上传图片命令优先走 onInsertUploading(coordinator 占位符+上传),
  退回 uploadFn 直接上传(无占位符)
- index.ts: SlashCommand.configure 注入 onInsertUploading,
  闭包延迟读取 this.coordinator(在 editor 创建后才实例化)
2026-06-22 15:30:39 +08:00
xfy
5d527778a5 feat(editor): wire UploadCoordinator and custom Image in TiptapEditorInstance
- index.ts: 替换 Image.configure 为 UploadImage(自定义扩展+NodeView)
- index.ts: editor 创建后实例化 UploadCoordinator 并注入 NodeView
- index.ts: FileHandler onPaste/onDrop 改走 coordinator.insertUploading
- index.ts: 暴露 removeUploadByUploadId 供 Rust ×关闭调用
- index.ts: destroy 清理 coordinator
- upload-image.ts: 修正 import 来源(Image 来自 extension-image, mergeAttributes 来自 core)
2026-06-22 15:28:40 +08:00
xfy
57bea05aa3 feat(editor): add custom Image extension with upload-state NodeView 2026-06-22 15:25:25 +08:00
xfy
8ce9c58997 feat(editor): maintain window.__tiptap_uploads for Rust polling 2026-06-22 15:24:37 +08:00
xfy
6506931c1d feat(editor): add runUpload and retryUpload to UploadCoordinator 2026-06-22 15:23:57 +08:00
xfy
e06667f792 feat(editor): add UploadCoordinator skeleton with pending map and node removal 2026-06-22 15:22:42 +08:00
xfy
c92ba6d648 docs(plan): editor image upload placeholder implementation plan
13 个任务的实现计划,对应 upload-placeholder spec:
- Task 1-3: UploadCoordinator(pending map / runUpload+retry / notifyRust)
- Task 4: 自定义 Image 扩展 + NodeView 类(三种态)
- Task 5-6: index.ts 接线 + slash-command 走 coordinator
- Task 7: NodeView 样式(遮罩/spinner/错误卡片)
- Task 8-12: write.rs(signal/轮询/提示/保存拦截/fetch 改造)
- Task 13: 全量验证(cargo test/clippy/dx check + 手动清单)

每个任务含完整代码、行号、构建/测试命令、commit。
2026-06-22 15:19:01 +08:00
xfy
055329107e docs(spec): editor image upload placeholder and failure feedback design
Some checks failed
CI / check (push) Failing after 4m45s
CI / build (push) Has been skipped
文章编辑器图片上传占位符与失败提示的设计文档

经 brainstorming 流程产出的 spec,涵盖:
- 自定义 Image 扩展 + NodeView 承载上传状态(uploading/error/done)
- UploadCoordinator 集中管理三个上传入口,按 upload-id 定位节点更新
- JS→Rust 轮询通道(window.__tiptap_uploads)传递上传事件与计数
- 顶部多条堆叠失败提示 + 编辑器内失败卡片(重试/移除)
- 保存拦截双重防护(counts 检查 + markdown 兜底扫描 blob:)
- write.rs fetch 改造,透传服务端中文错误消息

不含实现,仅设计。验收标准 11 条。
2026-06-22 15:09:38 +08:00
xfy
3df677e181 feat(comment): show real relative time on pending comments
待审核评论显示真实相对时间,而非永久"刚刚"

pending_item.rs 改用 format_relative_time_iso 基于创建时间动态计算
相对文本(承接上一个 relative time 重构 commit),并加 title 悬浮提示
显示原始 ISO 时间。修复待审核评论一直显示"刚刚"的问题。
2026-06-22 14:19:05 +08:00