Communitygithub.com

OthmanAdi/bookshelf

為網頁專案添加一個 3D 書架,包含可拖曳的書背列,作為書籤管理器、稍後閱讀清單或閱讀清單,其中儲存的連結會以書籍形式呈現,點擊後可開啟閱讀頁面。提供四個無執行時期相依性的 TypeScript 模組:CSS 3D 書架 (DOM,無需 WebGL)、彈簧與拖曳動作引擎、版本化的本機書籤儲存庫(支援匯入與匯出),以及可記住閱讀位置的安全文字閱讀器。

bookshelf 是什麼?

OthmanAdi/bookshelf 是一個獨特的網頁開發工具包,它將書籤與稍後閱讀清單管理轉化為一個沉浸式的 3D 書架體驗。它由四個相互獨立的 TypeScript 模組組成,沒有任何執行時期相依性:`shelf` 模組負責純 DOM 的 3D 書架渲染;`motion` 模組提供基於物理的彈簧與拖曳動畫;`store` 模組處理版本化的本機書籤儲存、驗證、匯入與匯出;`reader` 模組則提供一個安全的文字閱讀器,能記住閱讀位置。這些模組可以單獨使用,也能組合使用,為開發者提供極大的靈活性。此技能特別適合 Claude Code、Codex 或 Cursor 等 AI 代理工具,用於生成或整合這個可拖曳的 3D 書架到任何 Web 專案中。開發者可以要求一個完整的書架應用,或是只選用其中一個部分(例如僅使用 `motion` 模組來為自己的元件添加拖曳與快滑動畫)。其設計理念是模組間互不相知,透過一個主機端的組合檔案來協調,確保了程式碼的純淨與可維護性。

相容平台✓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根據姿勢繪製書本,回報意圖(聚焦、開啟、步進、跳躍)時間、手勢、儲存
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 <temporary folder>
    

    臨時資料夾是一個新的空目錄,位於專案外部(系統臨時目錄即可)。告訴使用者它的位置。它就是原始碼根目錄。從中複製,永遠不要在裡面編輯,並在最後說明可以將其刪除。如果複製失敗(沒有網路、沒有 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.tssrc/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-* 自訂屬性以獲取顏色、字體、間距和時序。要重新樣式化,請覆蓋 token,不要編輯模組的 CSS。Token 只定義值:頁面背景、正文字體和重置位於 src/styles/base.css。在一個空專案中,也複製該檔案(它會導入 tokens.css 和 fonts.css,因此如果跳過步驟 3,請移除 fonts 導入),或者自己在 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' 這一行,token 堆疊會回退到系統字體。
  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 以了解每種組合的可複製連接方式。最簡單的實用組合:shelf 加上 motion:

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

const motion = createShelfMotion({ count: items.length, reducedMotion: prefersReducedMotion });
const shelf = mountShelf(host, {
  items,
  label: 'My shelf',
  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 或 opacity 低於 1。任何一項都會展平 3D 上下文,導致書籍塌陷成一個平面。請淡化場景(透視元素)代替。
  • 書架會繪製給定的姿勢。沿著列的移動和開啟動作來自 setPose,它運行在 motion 模組擁有的影格循環上。對 .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。在以一定速度滑動釋放後,瀏覽器可能會吞掉下一次點擊的點擊事件。因此,shelf.ts 會在 pointerup 時啟動書本,條件是觸控從未離開 10 px 半徑且持續時間少於 700 毫秒,並跳過瀏覽器為同一次點擊發送的 click 事件(鍵盤點擊的 detail 為 0,始終計算)。gesture.ts 也會在拖曳鎖定後消耗 touchmove,以防止瀏覽器開始翻頁效果。
  • 閱讀器僅使用 createElement 和 textContent 建構 DOM。src/reader 中任何地方都沒有 innerHTML,並且有一個原始碼層級的測試來強制執行此規則。連結僅保留給 http: 和 https:。
  • Store 僅接受 http: 和 https: 地址,並且永遠不會請求它們。應用程式會顯示一個連結以及使用者貼上的任何文字。不要添加遠端請求來讓閱讀器「更智能」:瀏覽器會阻止對大多數網站的此類請求,而代理會改變安全模型。
  • 匯入永遠不會覆寫,並且來自外部的任何東西都不會在沒有預覽的情況下應用。書架檔案或書架連結來自陌生人:使用 readShelfFile 或 decodeShelfLink 讀取,顯示 summariseShelfFile,並且僅在使用者同意後才調用 importShelf。閱讀位置、已讀標記和私人筆記永遠不會進入書架檔案。
  • 減少動畫是其自身的路徑,沒有任何東西移動。motion 模組上的 reducedMotion 使移動立即完成,並將開啟和關閉轉變為 150 毫秒的淡入淡出。傳遞即時偏好設定(prefersReducedMotion),而不是一次讀取的值。
  • 始終在使用者可見的地方說明閱讀器邊界。沒有文字的連結必須顯示「開啟原始連結」和一個添加文字的一步操作方式。

5. 檢查結果

執行適用的檢查。references/checks.md 中包含了命令、用於斷言的 DOM 鉤子以及用於真實滑鼠和觸控拖曳的 Playwright 程式碼片段。

取用最低驗證
任何模組npx tsc --noEmit 乾淨通過,該模組自身的 *.test.ts 在 Vitest 搭配 happy-dom 下通過
shelf書本能渲染,data-focus 在 setPose 後改變,每個項目恰好一個 .bb-book__control,方向鍵觸發 step
shelf + motion在長到足以循環的列上,真實滑鼠拖曳會改變 data-focus-id 並以 data-settled="1" 結束。在靜止列上,方向鍵會改變它,而拖曳不會。垂直觸控拖曳會滾動頁面
readerEscape 鍵調用 onClose,重新載入還原 scrollTop,文字中的 <script> 顯示為文字
storejavascript: 地址被拒絕,匯出然後匯入可往返,在同一個儲存空間上第二個 createStore 能看到資料,書架能通過 encodeShelfLink 然後 decodeShelfLink 的處理
全部四個references/checks.md 中的設定冒煙測試通過。開啟一本書,滾動,重新載入:同一本書在同一個地方開啟。關閉:焦點回到那本書上

報告您執行了什麼以及沒有執行什麼。通過類型檢查的建置並未被證明可以拖曳。

外觀

paper(預設

相關技能