Communitygithub.com

OthmanAdi/bookshelf

为网页项目添加一个3D书架,这是一行可拖拽的书脊,用作书签管理器、稍后阅读列表或阅读列表,其中保存的链接是书籍,点击可打开阅读页面。提供四个无运行时依赖的 TypeScript 模块,包括:基于 CSS 3D 的书架(DOM,无需 WebGL)、弹簧与拖拽动效引擎、带版本控制的本地书签存储(支持导入导出)、以及会记住阅读位置的安全文本阅读器。在用户请求创建 3D 书架、可拖拽书脊行/轮播、以书籍形式显示的书签管理器或稍后阅读列表、封面飞入阅读视图并恢复阅读位置,或仅需要四个部分(书架、动效、存储、阅读器)之一以集成到现有项目时使用,即使他们从未提及此仓库名称。

bookshelf 是什么?

Bookmark Bookshelf 是一套由四个独立的 TypeScript 模块组成的工具集,专为构建以3D书架为视觉隐喻的书签管理或稍后阅读体验而设计。它无需任何运行时依赖,核心模块包括:`shelf` 负责在 DOM 中创建基于 CSS 3D 变换的书脊行,支持焦点、展开、步进和跳转等交互意图;`motion` 提供弹簧物理、拖拽、滑动、吸附及开合进度动画;`store` 实现了基于 localStorage 的版本化书签库,支持数据验证、迁移、导入导出、搜索和排序;`reader` 则是一个安全的纯文本阅读器,能够追踪和恢复阅读位置、调整文字大小、管理私人笔记及标记已读。这些模块之间没有直接依赖,可以通过组合使用,也可以独立集成到现有项目中。它支持所有主流的 Web 框架(如 React、Vue、Svelte),通过统一的 `mount(element, options)` API 进行挂载。该工具集特别适合需要创建可拖拽的 3D 书脊轮播、可视化书签管理器、或者带有阅读位置记忆功能的稍后阅读工具的开发场景。

兼容平台✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/OthmanAdi/bookmark-bookshelf/tree/HEAD/.agents/skills/bookshelf

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

书签书架

四个无运行时依赖的 TypeScript 小模块。每个模块可独立工作。组合起来,它们构成一个可拖拽、打开和阅读保存链接的书架。此技能描述版本 0.2.0。

模块文件夹功能不关心什么
shelfsrc/shelf根据姿态绘制书籍体积,报告意图(focus、open、step、jump)时间、手势、存储
motionsrc/motion弹簧、拖拽、轻拂、吸附、打开和关闭的进度书籍、DOM 结构
storesrc/store基于本地存储的版本化库:验证、迁移、导入导出、搜索、排序、单个书架作为文件或链接DOM、渲染
readersrc/reader将保存的文本显示为文本节点,跟踪并恢复位置、文字大小、私人笔记、标记为已读书架、存储

模块并不在此技能文件夹内。它们存在于 Bookmark Bookshelf 仓库的检出副本中,首要任务是找到它。将模块文件夹复制出来,不要重新输入它们。

0. 寻找源码根目录

按顺序尝试以下路径,使用第一个成功找到的。

  1. 技能旁边。如果此文件位于 <root>/.agents/skills/bookshelf/SKILL.md,且 <root>/src/shelf/index.ts 存在,则 <root> 即为源码根目录。当技能被原地使用,或作为保留完整仓库的插件安装时,属于此情况。

  2. 用户提供的路径。如果技能被复制到其他地方(如代理的技能文件夹),用户可能已有一个检出副本。如果请求中包含路径,则将其用作源码根目录。如果没有,则询问一个路径,并在同一问题中提供选项 3。

  3. 浅克隆,仅当用户同意后才执行。先询问,并清晰说明后续操作:将克隆公共仓库 https://github.com/OthmanAdi/bookmark-bookshelf 的发布标签 v0.2.0 到新的临时文件夹中,不下载其他任何内容。在用户同意之前不要获取任何内容。同意后,运行:

    git clone --depth 1 --branch v0.2.0 https://github.com/OthmanAdi/bookmark-bookshelf <临时文件夹>
    

    临时文件夹是一个在项目之外的新建空目录(系统临时目录即可)。告知用户其位置。它将成为源码根目录。从中复制文件,切勿在其中编辑,并在最后告知用户可以删除它。如果克隆失败(无网络、无 git、标签缺失),则说明情况并返回选项 2。

  4. 复制前验证。在源码根目录:

    • package.json 应包含 "name": "bookmark-bookshelf"。记录其 version。
    • 应存在以下标记文件:src/shelf/index.ts、src/motion/index.ts、src/store/index.ts、src/reader/index.ts、src/app/bookshelf.ts、src/app/bookshelf.css、src/app/flight.ts、src/styles/tokens.css。
    • 如果是 git 检出副本,记录 git -C <root> rev-parse --short HEAD。

    如果缺少某个标记文件,则停止并指出具体缺少哪个。此技能描述版本 0.2.x。对于其他版本,请先阅读每个模块的 types.ts,再信任此处的参考文件:0.1.0 版本没有 flight.ts,且其 shelf、store 和 reader 的约定不同(参见 references/modules.md 结尾)。

  5. 报告从中复制的源路径、版本和提交哈希,以及是否为自行创建的克隆,以便后续更新时进行比较。

以下每个以 src/、examples/ 或 public/ 开头的路径都是相对于该源码根目录的。

1. 首先确定范围

询问用户需要什么,然后只取所需部分。

他们想要取还要取
可拖拽的 3D 书脊行shelf、motionsrc/styles/tokens.css
带自定义点击处理的书架,无需拖拽shelftokens.css
可恢复位置的阅读视图readertokens.css、src/styles/base.css 中的 .bb-button、.bb-label 和 .bb-visually-hidden 规则
不带 UI 的书签库:导入、导出、搜索、分享书架store无
其他行元素的拖拽、轻拂和吸附效果motion无
可打开阅读器的书架,使用自己的数据shelf、motion、readertokens.css、base.css 中的相同三条规则
完整组合全部四个、src/app/bookshelf.ts、src/app/bookshelf.css、src/app/flight.ts如果需要示例链接,则取 src/styles/*、src/data/*

模块之间从不相互导入。如果在两个模块文件夹之间出现导入,则集成方式有误:汇合点应属于宿主自己的组合文件(参见 src/app/bookshelf.ts)。

完整组合包含七项内容,并且列表很容易遗漏一个:四个模块文件夹、src/app/bookshelf.ts、其样式表 src/app/bookshelf.css 以及 src/app/flight.ts(它在书架和页面之间运输封面)。脚本会导入其他两个,因此没有它们的构建会因文件缺失而失败。将这三个文件一起复制到与模块文件夹相邻的文件夹中。

围绕组合的应用(导航栏及其菜单、用于添加链接的对话框、批量导入和分享书架、列表视图)是 src/app/ 的其余部分以及 app.html。这些文件相互依赖,并依赖 app.html 中的元素 ID。整体复制并适应,或基于 store 的函数构建自己的界面,如 references/composition.md 所示。

2. 引入文件

  1. 将选定的每个文件夹从 src/ 原样复制(包括测试 *.test.ts)到宿主的源码树中。
  2. 复制 src/styles/tokens.css 并仅加载一次。模块读取 --bb-* 自定义属性用于颜色、排版、间距和时序。若要重新样式,请覆盖 tokens,不要编辑模块的 CSS。Tokens 仅定义值:页面背景、正文字体和重置样式位于 src/styles/base.css 中。在空项目中,也复制该文件(它会导入 tokens.css 和 fonts.css,因此如果跳过第 3 步,则删除字体导入),或者自己在 body 上设置 background: var(--bb-paper); color: var(--bb-ink); font-family: var(--bb-font-serif)。
  3. 字体是可选的。src/styles/fonts.css 打包了 Newsreader 和 IBM Plex Mono(SIL OFL 1.1),需要两个开发依赖:@fontsource-variable/newsreader 和 @fontsource/ibm-plex-mono。如果包含它们,请同时将许可证文本复制到 public/licenses/。不包含时,从 base.css 中删除 @import './fonts.css' 行,令牌栈将回退到系统字体。
  4. shelf.ts 和 reader.ts 会导入自身的 CSS(import './shelf.css')。需要一个能处理 CSS 导入的打包工具(如 Vite、webpack、Parcel、带有 CSS 加载器的 esbuild)。如果没有,请删除这些导入行,并从页面链接 CSS 文件:书架是 shelf.css、cover.css 和 variants.css,阅读器是 reader.css 和 reader-text.css。
  5. TypeScript:代码针对 strict、noUncheckedIndexedAccess 和 verbatimModuleSyntax 编写,使用 moduleResolution: "bundler"。在更宽松的设置下也能编译。
  6. 空项目的工具:npm i -D vite typescript vitest happy-dom @playwright/test,Node 版本 ^22.12.0 || ^24.0.0 || >=26.0.0。references/checks.md 包含最小的 Vitest 和 Playwright 配置。

框架宿主(React、Vue、Svelte):每个模块都通过 mount(element, options) 暴露,返回一个包含 destroy() 方法的句柄。在框架的“首次渲染后”钩子中挂载,在其清理过程中销毁。references/composition.md 提供了一个用于包装的普通函数。

3. 连接

阅读 references/modules.md 了解约定(输入、输出、事件),阅读 references/composition.md 了解每种组合的即用接线代码。最短的有用组合:书架加动效。

import { createShelfMotion, prefersReducedMotion } from './motion';
import { mountShelf } from './shelf';

const motion = createShelfMotion({ count: items.length, reducedMotion: prefersReducedMotion });
const shelf = mountShelf(host, {
  items,
  label: '我的书架',
  onLoopChange: (loop) => motion.setLoop(loop), // 书架决定行是否循环
});
motion.setLoop(shelf.loop);
motion.setTiming(shelf.variant.motion);

motion.subscribe((state) => {
  shelf.setPose({ position: state.position, open: 0, reduced: state.reduced });
  shelf.element.dataset.settled = state.settled ? '1' : '0'; // 允许测试等待静止
});
motion.attach(shelf.surface, {
  pxPerItem: () => shelf.pitch(),
  extent: () => shelf.surface.clientWidth,
  enabled: () => !shelf.fixed, // 短行保持静止,因此不能拖拽
});

shelf.on((intent) => {
  if (intent.type === 'focus') motion.goTo(intent.slot);
  else if (intent.type === 'step') motion.step(intent.direction);
  else if (intent.type === 'jump') motion.goTo(intent.to === 'first' ? 0 : items.length - 1);
  else openSomething(intent.id);
});

items 是一个由 { id, title, source?, author?, cloth?, pattern?, seed?, progress?, thickness?, stature?, outline?, note?, done? } 组成的数组。各组合的可运行版本位于 examples/ 中。

是否循环由书架决定。只有当行的书籍体积填满场景至少五分之四时,行才会循环。短行静止在两个书档之间(shelf.fixed):箭头、方向键和点击书脊可选择显示的书籍,拖拽无效。根据项目数量告知用户将看到哪种情况,因为桌面屏幕上的八个标题会静止。

4. 保持其正常运行的规则

这些规则一旦更改就会破坏功能。每项都有原因。

  • .bb-shelf__row 和书籍正面之间不得设置 overflow、filter、clip-path、mask、contain: paint 或低于 1 的 opacity。其中任何一个都会扁平化 3D 上下文,导致书籍坍缩成平面。请淡出场景(perspective 元素)代替。
  • 书架按照给定的姿态绘制。行移动和打开来自 setPose,动效模块拥有帧循环。向 .bb-book 添加 CSS 过渡会与该循环冲突。悬停效果应设置在 .bb-book__body 上。
  • 书架不负责运输封面。当 pose.open 大于 0 时,它会隐藏居中的书籍体积并分开其邻居。运输封面是宿主的职责(src/app/flight.ts):一个副本放在固定层,仅通过 transform 移动,从 shelf.coverPlacement() 到 reader.flightTarget()。书架的书籍、副本和阅读器自身的封面绝不能同时可见。
  • 每个项目一个原生按钮,而非每个渲染的书籍一个。循环行会渲染副本。只有真实副本携带 button.bb-book__control,副本是 aria-hidden 的。正面是普通的 span,按钮是它们前面一个独立且不可见的元素。不要使正面可获得焦点,也不要用 <button> 包裹正面。
  • 在拖拽表面上保持 touch-action: pan-y。使用 none 时,放在书架上的手指将无法再滚动页面。
  • 触控拖拽期间,忽略来自后代元素的 lostpointercapture。手指下的元素会隐式捕获指针,表面取得捕获后,其后代的丢失事件会冒泡。gesture.ts 检查 event.target === element。保留此检查。
  • 触摸点击书籍通过指针事件识别,而非 click。在高速滑动释放后,浏览器可能会吞没下一次点击的 click 事件。因此 shelf.ts 在 pointerup 上激活书籍,条件是触摸从未离开 10 像素半径且持续时间低于 700 毫秒,并跳过浏览器针对同一点击发送的 click 事件(键盘点击的 detail 为 0,始终有效)。gesture.ts 还会在拖拽锁定后使用 touchmove,以防止浏览器启动滑动。
  • 阅读器仅使用 createElement 和 textContent 构建 DOM。src/reader 中没有任何 innerHTML,并且有一个源码级测试强制执行此项。链接仅保留 http: 和 https: 的。
  • 存储只接受 http: 和 https: 地址,并且从不获取它们。应用显示链接和用户粘贴的任何文本。不要添加远程获取让阅读器“更智能”:浏览器会阻止大多数站点的请求,并且代理会改变安全模型。
  • 导入永远不会覆盖,来自外部的任何内容未经预览就不会被应用。书架文件或书架链接来自陌生人:使用 readShelfFile 或 decodeShelfLink 读取,显示 summariseShelfFile,并仅在用户同意后调用 importShelf。阅读位置、已读标记和私人笔记永远不会进入书架文件。
  • 减少动效是一个独立的路径,没有任何内容需要移动。动效模块的 reducedMotion 使移动瞬间完成,并将打开和关闭变成 150 毫秒的淡入淡出。传递实时偏好(prefersReducedMotion),而非仅读取一次的值。
  • 始终向用户说明阅读器的边界。没有文本的链接必须显示“打开原文”并提供添加文本的一步式方法。

5. 检查结果

运行适用的测试。references/checks.md 包含命令、用于断言的 DOM 钩子以及用于真实鼠标和触控拖拽的 Playwright 代码片段。

获取了最低验证
任何模块npx tsc --noEmit 无错误,模块自身的 *.test.ts 在具有 happy-dom 的 Vitest 下通过
shelf书籍渲染,setPose 更改 data-focus,每个项目恰好有一个 .bb-book__control,方向键触发 step
shelf + motion对于足够长以循环的行,真实鼠标拖拽会更改 data-focus-id 并以 data-settled="1" 结束。对于静止的行,方向键会更改它,拖拽无效。垂直触摸拖拽会滚动页面
readerEscape 调用 onClose,重新加载恢复 scrollTop,文本中的 <script> 显示为文本
storejavascript: 地址被拒绝,导出然后导入可往返,同一存储上的第二个 createStore 能看到数据,书架可经受 encodeShelfLink 然后 decodeShelfLink
全部四个references/checks.md 中的设置冒烟测试通过。打开一本书,滚动,重新加载:同一本书在同一位置打开。关闭:焦点回到那本书

报告运行了哪些测试以及未运行哪些。通过类型检查的构建并不表示拖拽功能正常。

外观

paper(默认)

…

[已截断]

相关技能