ブックマーク本棚 は何をしますか?
依存関係のない4つの小さなTypeScriptモジュール。それぞれ単独で動作します。まとめると、保存したリンクをドラッグ、開封、読書できる本棚になります。このスキルはバージョン0.2.0を説明します。
| モジュール | フォルダ | 動作 | 認識しないもの |
|---|---|---|---|
| shelf | src/shelf | ポーズに応じて本を描画し、インテント(フォーカス、開く、ステップ、ジャンプ)を報告する | 時間、ジェスチャー、ストレージ |
| motion | src/motion | バネ、ドラッグ、フリック、スナップ、開閉の進行度 | 本、DOM構造 |
| store | src/store | ローカルストレージ内のバージョン管理されたライブラリ:検証、移行、インポート/エクスポート、検索、ソート、ファイルまたはリンクとしての本棚1つ | DOM、レンダリング |
| reader | src/reader | 保存されたテキストをテキストノードとして表示し、位置、テキストサイズ、プライベートメモ、「既読」を追跡および復元する | 本棚、ストレージ |
モジュールはこのスキルフォルダ内にはありません。ブックマーク本棚リポジトリのチェックアウトにあり、最初の仕事はそれを見つけることです。モジュールフォルダをそこからコピーし、再入力しないでください。
0. ソースルートを見つける
以下のルートを順に試し、最初に成功したところで停止します。
-
スキルの隣。このファイルが
<root>/.agents/skills/bookshelf/SKILL.mdにあり、<root>/src/shelf/index.tsが存在する場合、<root>がソースルートです。これはスキルがその場で使用されている場合、またはリポジトリ全体を保持するプラグインとしてインストールされている場合です。 -
ユーザーが指定するパス。スキルが別の場所(エージェントのスキルフォルダ)にコピーされた場合、ユーザーはすでにチェックアウトを持っている可能性があります。リクエストにパスが含まれている場合は、それをソースルートとして使用します。含まれていない場合は、パスを尋ね、同じ質問でルート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に戻ります。
-
コピー前に確認します。ソースルートで:
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の末尾を参照)。 -
コピー元のソースパス、バージョン、コミット、および自分が作成したクローンかどうかを報告し、後で更新を比較できるようにします。
以下の src/、examples/、public/ で始まるすべてのパスは、そのソースルートからの相対パスです。
1. 最初にスコープを決定する
ユーザーが必要なものを尋ね、それだけを取得します。
| ユーザーの希望 | 取得するもの | さらに取得するもの |
|---|---|---|
| ドラッグ可能な3D背表紙の行 | shelf、motion | src/styles/tokens.css |
| 独自のクリック処理を持つ本棚(ドラッグなし) | shelf | tokens.css |
| 再開機能付きの読書ビュー | reader | tokens.css、src/styles/base.css の .bb-button、.bb-label、.bb-visually-hidden ルール |
| UIなしのブックマークライブラリ:インポート、エクスポート、検索、本棚の共有 | store | なし |
| 他の行のためのドラッグ、フリック、スナップ | motion | なし |
| 独自データでリーダーに開く本棚 | shelf、motion、reader | tokens.css、base.css の同じ3つのルール |
| 全体構成 | 4つすべて、src/app/bookshelf.ts、src/app/bookshelf.css、src/app/flight.ts | src/styles/*、例のリンクが必要な場合は src/data/* |
モジュールは互いにインポートしません。2つのモジュールフォルダ間にインポートがある場合、統合が間違っています:接合点はホスト自身の構成ファイル(src/app/bookshelf.ts を参照)に属します。
全体構成は7つのファイルで構成され、リストを1つ忘れやすいです:4つのモジュールフォルダ、src/app/bookshelf.ts、そのスタイルシート src/app/bookshelf.css、およびカバーを本棚とページ間で運ぶ src/app/flight.ts。スクリプトは他の2つをインポートするため、それらなしでビルドするとファイル欠落エラーで失敗します。3つのファイルをまとめて、モジュールフォルダの隣に配置するフォルダにコピーします。
構成の周りのアプリ(バーとそのメニュー、リンク追加、複数リンクの取り込みと本棚の共有のためのダイアログ、リストビュー)は、src/app/ の残りと app.html です。これらのファイルは相互に依存し、app.html の要素IDにも依存します。全体として取得して適応させるか、ストアの関数に基づいて独自のインターフェースを構築します。references/composition.md にその方法が示されています。
2. ファイルを取り込む
- 選択した各フォルダを
src/からそのままコピーし(テスト*.test.tsを含む)、ホストのソースツリーに配置します。 src/styles/tokens.cssをコピーし、1回だけ読み込みます。モジュールは色、タイプ、スペーシング、タイミングに--bb-*カスタムプロパティを読み取ります。再スタイルするには、トークンをオーバーライドし、モジュールCSSを編集しないでください。トークンは値のみを定義します:ページの背景、ボディフォント、リセットはsrc/styles/base.cssにあります。空のプロジェクトでは、そのファイルもコピーします(tokens.cssとfonts.cssをインポートするため、手順3をスキップする場合はfonts importを削除します)。または、bodyにbackground: var(--bb-paper); color: var(--bb-ink); font-family: var(--bb-font-serif)を自分で設定します。- フォントはオプションです。
src/styles/fonts.cssは Newsreader と IBM Plex Mono(SIL OFL 1.1)をバンドルし、2つの開発依存関係が必要です:@fontsource-variable/newsreaderと@fontsource/ibm-plex-mono。これらを配布する場合は、ライセンステキストもpublic/licenses/に含めてください。含めない場合は、base.cssから@import './fonts.css'の行を削除し、トークンスタックはシステムフォントにフォールバックします。 shelf.tsとreader.tsはCSSをインポートします(import './shelf.css')。これにはCSSインポートを処理するバンドラー(Vite、webpack、Parcel、CSSローダー付きesbuild)が必要です。バンドラーがない場合は、それらのインポート行を削除し、CSSファイルをページからリンクします:shelf用にshelf.css、cover.css、variants.css、reader用にreader.css、reader-text.css。- TypeScript:コードは
strict、noUncheckedIndexedAccess、verbatimModuleSyntax、およびmoduleResolution: "bundler"で記述されています。より緩い設定でもコンパイルされます。 - 空のプロジェクト用ツール:
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/ にあります。
ループは本棚が決定します。行は、その本がシーンの少なくとも4/5を埋める場合にのみループします。短い行は2つのブックエンドの間の中央に静止します(shelf.fixed):矢印、矢印キー、背表紙のクリックで表示される本を選択し、ドラッグは何もしません。アイテム数に応じてどちらが表示されるかをユーザーに伝えます。なぜなら、デスクトップ画面で8タイトルは静止するからです。
4. 動作を維持するルール
これらは変更すると壊れるものです。それぞれに理由があります。
.bb-shelf__rowと本の表面の間にある要素は、overflow、filter、clip-path、mask、contain: paint、またはopacityを1未満に設定してはいけません。これらのいずれかが3Dコンテキストを平坦化し、本が平面に崩れます。代わりにシーン(パースペクティブ要素)をフェードアウトします。- 本棚は与えられたポーズを描画します。行に沿った移動と開閉は、motionモジュールが所有するフレームループ上の
setPoseから来ます。.bb-bookにCSSトランジションを追加すると、そのループと競合します。ホバー効果は.bb-book__bodyに適用します。 - 本棚はカバーを飛行させません。
pose.openが0より大きい間、中央の本を非表示にし、隣の本を分離します。移動するカバーはホストの仕事です(src/app/flight.ts):固定レイヤー上に1つのコピーをtransformのみで移動させ、shelf.coverPlacement()からreader.flightTarget()へ。本棚の本、コピー、リーダー自身のカバーが同時に表示されることはありません。 - レンダリングされた本ごとではなく、アイテムごとに1つのネイティブボタン。ループする行はコピーをレンダリングします。実際のコピーのみが
button.bb-book__controlを持ち、コピーはaria-hiddenです。表面はプレーンなspanであり、ボタンはそれらの前にある別の非表示要素です。表面をフォーカス可能にしたり、<button>で表面をラップしたりしないでください。 - ドラッグサーフェスに
touch-action: pan-yを維持します。noneの場合、本棚に触れた指がページをスクロールできなくなります。 - タッチドラッグ中は、子孫からの
lostpointercaptureを無視します。指の下の要素が暗黙的にポインターをキャプチャし、サーフェスがキャプチャを取得すると、その子孫の損失イベントがバブルアップします。gesture.tsはevent.target === elementをチェックします。このチェックを維持します。 - 本へのタッチタップは、
clickではなくポインターイベントから認識されます。速度を伴うスワイプの後、ブラウザは次のタップのクリックを飲み込む可能性があります。shelf.tsはしたがって、タッチが10px半径内を離れず700ms未満だった場合にpointerupで本をアクティブにし、ブラウザがその同じタップに対して送信するクリックをスキップします(detailが0のキーボードクリックは常にカウントされます)。gesture.tsはドラッグがロックされるとtouchmoveも消費し、ブラウザのフリングが開始されないようにします。 - リーダーは
createElementとtextContentのみでDOMを構築します。src/reader内にはinnerHTMLはどこにもなく、ソースレベルのテストがそれを強制します。リンクはhttp:とhttps:のみ保持されます。 - ストアは
http:とhttps:アドレスのみを受け付け、決してフェッチしません。アプリはリンクとユーザーが貼り付けたテキストを表示します。リモートフェッチを追加してリーダーを「賢く」しないでください:ブラウザはほとんどのサイトでそれをブロックし、プロキシはセキュリティモデルを変更します。 - インポートは決して上書きせず、外部から来たものはプレビューなしで適用されません。本棚ファイルまたは本棚リンクは見知らぬ人から来ます:
readShelfFileまたはdecodeShelfLinkで読み取り、summariseShelfFileを表示し、ユーザーが同意した後にのみimportShelfを呼び出します。読書位置、既読マーク、プライベートメモは本棚ファイルに決して含まれません。 - モーション削減は独自のパスであり、何も移動しません。motionモジュールの
reducedMotionは移動を即座にし、開閉を150msのフェードにします。ライブの設定(prefersReducedMotion)を渡し、一度だけ読み取った値を渡さないでください。 - ユーザーがリーダーの境界を常に確認できる場所に表示します。テキストのないリンクは「オリジナルを開く」とテキストを追加するためのワンステップの方法を表示する必要があります。
5. 結果を確認する
該当するものを実行します。references/checks.md にコマンド、アサートするDOMフック、実際のマウスおよびタッチドラッグ用のPlaywrightスニペットがあります。
| 取得したもの | 最小限の証明 |
|---|---|
| 任意のモジュール | npx tsc --noEmit がクリーン、モジュール自身の *.test.ts がVitestと happy-dom でパス |
| shelf | 本がレンダリングされ、data-focus が setPose で変更され、アイテムごとに正確に1つの .bb-book__control、矢印キーが step を発行 |
| shelf + motion | ループするのに十分長い行で、実際のマウスドラッグが data-focus-id を変更し、data-settled="1" で終了。静止行で矢印キーが変更し、ドラッグは変更しない。垂直タッチドラッグがページをスクロール |
| reader | Escapeが onClose を呼び出し、リロードが scrollTop を復元し、テキスト内の <script> がテキストとして表示 |
| store | javascript: アドレスが拒否され、エクスポート後にインポートがラウンドトリップし、同じストレージでの2回目の createStore がデータを認識し、本棚が encodeShelfLink 後に decodeShelfLink でサバイブ |
| 4つすべて | references/checks.md のセットアップスモークチェックがパス。ボリュームを開き、スクロール、リロード:同じボリュームが同じ場所で開く。閉じる:フォーカスがそのボリュームに戻る |
実行したものとしなかったものを報告します。型チェックを通ったビルドは、ドラッグが示されていません。
見た目
paper(デフォルト...