600 Commits

Author SHA1 Message Date
xfy
eed0b24ec3 Merge branch 'refactor/lightbox-modernize'
Some checks failed
CI / check (push) Failing after 5m14s
CI / build (push) Has been skipped
灯箱 index.ts 现代化重构。先补 happy-dom 行为回归测试(11 个黑盒用例覆盖
循环闭包捕获、打开/关闭、图集循环边界、单张不切换),再做重构、用 23 个
测试全绿作证行为不变:
- var → const/let
- for+var+IIFE 包装 → for..of/forEach + const
- 字符串拼接 → 模板字面量
- 拆外层 IIFE(ES module 作用域已封装)
2026-06-24 13:17:18 +08:00
xfy
25eacc1c3c refactor(lightbox): modernize to const/let, template literals, drop IIFE
纯现代化重构,行为不变(23 个回归测试全绿作证):
- var → const/let(state、gotoIndex 的 newIndex 用 let,其余 const)
- 循环 for+var+IIFE包装 → for..of/forEach + const(天然捕获迭代变量,
  消除旧 var 循环闭包必须立即执行函数固定变量的写法)
- 字符串拼接 → 模板字面量(counter 文本、transform、width/height)
- 事件处理 function() → 箭头函数(无 this 依赖场景)
- 拆掉外层 (function(){'use strict'; ... })():ES module 作用域已封装,
  IIFE 是历史遗留;辅助函数仍用 function 声明(openLightbox↔closeLightbox
  互递归依赖提升)

几何纯函数(fitCentered/transformFor/originalUrl)已在 geometry.ts,
本文件只剩 DOM 胶水 + state 单例。
2026-06-24 11:44:06 +08:00
xfy
aadfed2595 test(lightbox): add happy-dom behavior regression tests for refactor
为现代化重构(var→const、拆 IIFE、for→for..of)补行为回归防线。
黑盒驱动 window.__initLightbox,覆盖高风险路径:
- 循环闭包捕获 idx(点第 1/2/3 张 counter 显示正确序号)
- 点击打开(overlay 创建、origSrc 去 query、caption、单张 counter 隐藏)
- 关闭(Esc / 点背景;点图片本身不关)
- 图集 gotoIndex 循环边界(首末衔接 + originNode 更新后焦点归还)
- 单张图不参与切换

happy-dom 20.10.6 作 devDep,vitest 默认环境改 happy-dom;
geometry.test.ts 用 // @vitest-environment node 指令保持纯函数在 node 跑。

23 tests (12 geometry + 11 behavior) 全绿。
2026-06-24 11:40:36 +08:00
xfy
ffaf191398 Merge branch 'feat/lightbox-typescript-build'
灯箱 TypeScript 化与独立构建:将 public/js/lightbox.js (540 行 JS) 迁移为
libs/lightbox/ 独立 TS 工程,镜像 libs/tiptap-editor/ 结构。Vite 构建到
public/lightbox/,经 Dioxus.toml 全局注入,Rust 端去 include_str! 改用
__lightboxSelectors 配置 + IIFE 自启动双保险契约。

- geometry.ts (纯函数 + 12 Vitest 用例) / index.ts (DOM 胶水 + state)
- .lightbox-* CSS 搬入 lightbox.css;.blur-img* 留 input.css (列表页依赖)
- 依赖: typescript 6.0.3 / vite 8.1.0 / vitest 4.1.9 (npm latest)
- 修复关闭灯箱时 focus() 触发的页面滚动 (preventScroll)
2026-06-24 11:31:48 +08:00
xfy
507d271a2b fix(lightbox): prevent page scroll on close via focus({ preventScroll: true })
关闭灯箱时 removeOverlay() 调用 .focus() 归还焦点给原图,浏览器的 focus()
默认会 scrollIntoView,导致页面自动滚动把原图完整纳入视口。用户向下滚动后
只点露出的下半截图、再 Esc 关闭时,页面会向上跳到整图可见——非预期行为。

focus({ preventScroll: true }) 抑制该滚动(Chrome68+/FF68+/Safari15.4+ 支持),
焦点归还的无障碍语义不变。
2026-06-24 11:27:15 +08:00
xfy
05e2989278 build(lightbox): use npm install (npm 11 ci rejects cross-platform rolldawn optional deps) 2026-06-24 11:18:40 +08:00
xfy
022b63aedc refactor(lightbox): remove legacy public/js/lightbox.js (migrated to libs/lightbox) 2026-06-24 11:07:46 +08:00
xfy
0301185a17 refactor(post-content): drop lightbox include_str!, use config-driven init 2026-06-24 11:07:24 +08:00
xfy
adf9e9cf07 build: add build-lightbox targets, wire into build/build-linux/dev/test 2026-06-24 11:06:03 +08:00
xfy
3284b03819 build: register lightbox assets in Dioxus.toml, gitignore public/lightbox 2026-06-24 11:04:29 +08:00
xfy
b4ef906141 refactor(lightbox): move .lightbox-* CSS into lightbox project, wire import 2026-06-24 11:02:44 +08:00
xfy
5f57d375ea feat(lightbox): migrate DOM logic to TypeScript index.ts with self-start contract 2026-06-24 10:57:23 +08:00
xfy
44fc641aaa feat(lightbox): extract geometry pure fns with vitest coverage 2026-06-24 10:52:48 +08:00
xfy
c5af4457e2 feat(lightbox): scaffold libs/lightbox TS project (vite + tsc + vitest) 2026-06-24 10:49:58 +08:00
xfy
1cc09e4cbc fix(bridge): use unchecked_into for TiptapEditorModule (not dyn_into)
Some checks failed
CI / check (push) Failing after 4m44s
CI / build (push) Has been skipped
dyn_into runs 'instanceof TiptapEditorModule', but the IIFE TiptapEditor
is a plain object literal — not an instance of wasm-bindgen's registered
constructor — so the check always fails with a panic.

unchecked_into does only the compile-time type annotation; Reflect::get
already guarantees we have the right object.
2026-06-24 10:38:33 +08:00
xfy
410d594ac0 fix(bridge): read window.TiptapEditor via Reflect.get, not function call
The extern 'fn get_module() -> TiptapEditorModule' was compiled by
wasm-bindgen to 'window.TiptapEditor()' — a function call. But
TiptapEditor is the IIFE module object, not a function, so this threw
'window.TiptapEditor is not a function'.

Replace with js_sys::Reflect::get(&window, "TiptapEditor") +
dyn_into, which reads the property without invoking it.

This bug predated the Vite 8 upgrade but only surfaced after the
EditorOptions fix let initialization progress further.
2026-06-24 10:35:11 +08:00
xfy
fb31ecb5ae fix(tiptap): expose EditorOptions as runtime class for wasm-bindgen
EditorOptions was a TS interface (erased at runtime), but the Rust
wasm-bindgen extern uses #[wasm_bindgen(constructor)] which generates
'new EditorOptions()' — requiring a global constructor.

Vite 5/Rollup accidentally leaked the name into IIFE global scope,
masking this. Vite 8/Rolldown strictly erases interfaces, exposing it
as 'EditorOptions is not defined'.

Fix: make EditorOptions a class (retains runtime constructor) and mount
it on window, since the IIFE bundle name is taken by TiptapEditor.
2026-06-24 10:27:07 +08:00
xfy
368a651fa6 build: make test runs both cargo test and vitest 2026-06-24 10:14:20 +08:00
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