Bookmark Bookshelf
Quatre petits modules TypeScript sans dépendances d'exécution. Chacun fonctionne seul. Ensemble, ils forment une étagère de liens sauvegardés que vous faites glisser, ouvrez et lisez. Cette compétence décrit la version 0.2.0.
| Module | Dossier | Fait | Ne connaît rien à propos de |
|---|---|---|---|
| shelf | src/shelf | Dessine les volumes pour une pose, signale les intentions (focus, open, step, jump) | le temps, les gestes, le stockage |
| motion | src/motion | Ressorts, glissement, lancer, accrochage, progression d'ouverture et de fermeture | livres, structure du DOM |
| store | src/store | Bibliothèque versionnée dans le stockage local : validation, migration, import et export, recherche, tri, une étagère sous forme de fichier ou de lien | DOM, rendu |
| reader | src/reader | Affiche le texte sauvegardé sous forme de nœuds de texte, suit et restaure la position, la taille du texte, les notes privées, « Marquer comme lu » | étagères, stockage |
Les modules ne se trouvent pas dans ce dossier de compétence. Ils résident dans une copie du dépôt Bookmark Bookshelf, et la première tâche est de le trouver. Copiez les dossiers des modules à partir de celui-ci et ne les retapez pas.
0. Trouver la racine source
Essayez les chemins dans cet ordre et arrêtez-vous au premier qui fonctionne.
-
À côté de la compétence. Si ce fichier se trouve à
<root>/.agents/skills/bookshelf/SKILL.mdet que<root>/src/shelf/index.tsexiste,<root>est la racine source. C'est le cas lorsque la compétence est utilisée sur place, et lorsqu'elle a été installée comme un plugin qui conserve l'intégralité du dépôt. -
Un chemin donné par la personne. Si la compétence a été copiée ailleurs (le dossier des compétences d'un agent), la personne peut déjà avoir une copie du dépôt. Si la demande contient un chemin, utilisez-le comme racine source. Sinon, demandez-en un, et proposez la route 3 dans la même question.
-
Un clone superficiel, uniquement après l'accord de la personne. Demandez d'abord, et dites clairement ce qui va se passer : le dépôt public
https://github.com/OthmanAdi/bookmark-bookshelfest cloné au tag de versionv0.2.0dans un nouveau dossier temporaire, et rien d'autre n'est téléchargé. Ne récupérez rien avant que la personne ne dise oui. Après un oui, exécutez :git clone --depth 1 --branch v0.2.0 https://github.com/OthmanAdi/bookmark-bookshelf <dossier temporaire>Le dossier temporaire est un nouveau répertoire vide en dehors du projet (le répertoire temporaire du système convient). Dites à la personne où il se trouve. C'est la racine source. Copiez à partir de celui-ci, ne modifiez jamais à l'intérieur, et dites à la fin qu'il peut être supprimé. Si le clone échoue (pas de réseau, pas de git, tag manquant), dites-le et revenez à la route 2.
-
Vérifiez avant de copier. Dans la racine source :
package.jsona"name": "bookmark-bookshelf". Notez saversion.- Ces marqueurs existent :
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. - S'il s'agit d'une copie git, notez
git -C <root> rev-parse --short HEAD.
Si un marqueur manque, arrêtez-vous et dites lequel. Cette compétence décrit la version
0.2.x. Pour une autre version, lisez letypes.tsde chaque module avant de faire confiance aux fichiers de référence ici : 0.1.0 n'a pas deflight.ts, et ses contrats pour shelf, store et reader diffèrent (voir la fin dereferences/modules.md). -
Rapporter le chemin source, la version et le commit à partir desquels vous avez copié, et si c'était un clone que vous avez fait, afin qu'une mise à jour ultérieure puisse être comparée.
Chaque chemin ci-dessous qui commence par src/, examples/ ou public/ est relatif à cette racine source.
1. Décider d'abord de la portée
Demandez ce dont la personne a besoin, puis ne prenez que cela.
| Ils veulent | Prenez | Prenez aussi |
|---|---|---|
| Une rangée 3D de livres qu'ils peuvent faire glisser | shelf, motion | src/styles/tokens.css |
| Une étagère avec leur propre gestion des clics, pas de glissement | shelf | tokens.css |
| Une vue de lecture avec reprise | reader | tokens.css, les règles .bb-button, .bb-label et .bb-visually-hidden de src/styles/base.css |
| Une bibliothèque de signets sans UI : import, export, recherche, partage d'une étagère | store | rien |
| Glissement, lancer et accrochage pour une autre rangée | motion | rien |
| Une étagère qui s'ouvre dans un lecteur, avec leurs propres données | shelf, motion, reader | tokens.css, les mêmes trois règles de base.css |
| La composition complète | les quatre, src/app/bookshelf.ts, src/app/bookshelf.css, src/app/flight.ts | src/styles/*, src/data/* si les exemples de liens sont souhaités |
Les modules ne s'importent jamais mutuellement. Si une importation entre deux dossiers de modules apparaît, l'intégration est erronée : le point de rencontre appartient au propre fichier de composition de l'hôte (voir src/app/bookshelf.ts).
La composition complète comprend sept éléments, et la liste est facile à avoir un de moins : les quatre dossiers de modules, src/app/bookshelf.ts, sa feuille de style src/app/bookshelf.css, et src/app/flight.ts, qui transporte la couverture entre l'étagère et la page. Le script importe les deux autres, donc une construction sans eux échoue avec une erreur de fichier manquant. Copiez les trois fichiers ensemble, dans un dossier qui se trouve à côté des dossiers de modules.
L'application autour de la composition (la barre et ses menus, les boîtes de dialogue pour ajouter des liens, en importer plusieurs et partager une étagère, la vue liste) est le reste de src/app/ avec app.html. Ces fichiers dépendent les uns des autres et des identifiants d'éléments dans app.html. Prenez-les dans leur ensemble et adaptez, ou construisez votre propre interface sur les fonctions du store, comme le montre references/composition.md.
2. Importer les fichiers
- Copiez chaque dossier choisi de
src/inchangé, tests inclus (*.test.ts), dans l'arborescence source de l'hôte. - Copiez
src/styles/tokens.csset chargez-le une fois. Les modules lisent les propriétés personnalisées--bb-*pour la couleur, la police, l'espacement et le timing. Pour restyler, surchargez les tokens et ne modifiez pas le CSS des modules. Les tokens ne définissent que des valeurs : le fond de la page, la police du corps et les réinitialisations se trouvent danssrc/styles/base.css. Dans un projet vide, copiez également ce fichier (il importetokens.cssetfonts.css, donc supprimez l'import des polices si vous sautez l'étape 3), ou définissezbackground: var(--bb-paper); color: var(--bb-ink); font-family: var(--bb-font-serif)surbodyvous-même. - Les polices sont optionnelles.
src/styles/fonts.cssregroupe Newsreader et IBM Plex Mono (SIL OFL 1.1) et nécessite deux dépendances de développement :@fontsource-variable/newsreaderet@fontsource/ibm-plex-mono. Si vous les incluez, incluez également les textes de licence danspublic/licenses/. Sans elles, supprimez la ligne@import './fonts.css'debase.csset les piles de tokens reviennent aux polices système. shelf.tsetreader.tsimportent leur CSS (import './shelf.css'). Cela nécessite un bundler qui gère les importations CSS (Vite, webpack, Parcel, esbuild avec un chargeur CSS). Sans un, supprimez ces lignes d'importation et liez les fichiers CSS depuis la page :shelf.css,cover.cssetvariants.csspour l'étagère,reader.cssetreader-text.csspour le lecteur.- TypeScript : le code est écrit pour
strict,noUncheckedIndexedAccessetverbatimModuleSyntax, avecmoduleResolution: "bundler". Il compile également avec des paramètres moins stricts. - Outillage pour un projet vide :
npm i -D vite typescript vitest happy-dom @playwright/test, Node^22.12.0 || ^24.0.0 || >=26.0.0.references/checks.mdcontient la configuration minimale pour Vitest et Playwright.
Hôtes de framework (React, Vue, Svelte) : chaque module est mount(element, options) retournant un gestionnaire avec destroy(). Montez dans le hook « après le premier rendu » du framework et détruisez dans son nettoyage. references/composition.md a une fonction simple pour envelopper.
3. Câbler
Lisez references/modules.md pour les contrats (entrées, sorties, événements) et references/composition.md pour le câblage prêt à copier de chaque combinaison. Le plus court utile, étagère plus mouvement :
import { createShelfMotion, prefersReducedMotion } from './motion';
import { mountShelf } from './shelf';
const motion = createShelfMotion({ count: items.length, reducedMotion: prefersReducedMotion });
const shelf = mountShelf(host, {
items,
label: 'Mon étagère',
onLoopChange: (loop) => motion.setLoop(loop), // l'étagère décide si la rangée boucle
});
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'; // permet aux tests d'attendre le repos
});
motion.attach(shelf.surface, {
pxPerItem: () => shelf.pitch(),
extent: () => shelf.surface.clientWidth,
enabled: () => !shelf.fixed, // une rangée courte reste immobile, donc un glissement ne doit pas la déplacer
});
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 est un tableau de { id, title, source?, author?, cloth?, pattern?, seed?, progress?, thickness?, stature?, outline?, note?, done? }. Des versions exécutables des combinaisons se trouvent dans examples/.
Le bouclage est décidé par l'étagère. Une rangée boucle uniquement lorsque ses volumes remplissent au moins quatre cinquièmes de la scène. Une rangée plus courte reste immobile au milieu entre deux serre-livres (shelf.fixed) : les flèches, les touches fléchées et un clic sur un livre choisissent le volume présenté, et un glissement ne fait rien. Dites à la personne lequel des deux elle verra avec son nombre d'éléments, car huit titres sur un écran de bureau restent immobiles.
4. Règles qui le maintiennent en état de marche
Ce sont les choses qui cassent lorsqu'elles sont modifiées. Chacune a une raison.
- Rien entre
.bb-shelf__rowet une face de livre ne peut définiroverflow,filter,clip-path,mask,contain: paintouopacityinférieur à 1. Chacun d'eux aplatit le contexte 3D et les livres s'effondrent en un plan. Estompez la scène (l'élément de perspective) à la place. - L'étagère dessine la pose qui lui est donnée. Le mouvement le long de la rangée et l'ouverture proviennent de
setPosesur une boucle d'images appartenant au module de mouvement. L'ajout de transitions CSS à.bb-bookentre en conflit avec cette boucle. Les effets de survol vont sur.bb-book__body. - L'étagère ne fait pas voler la couverture. Pendant que
pose.openest supérieur à 0, elle cache le volume centré et écarte ses voisins. Une couverture voyageante est le travail de l'hôte (src/app/flight.ts) : une copie sur une couche fixe, déplacée uniquement partransform, deshelf.coverPlacement()àreader.flightTarget(). Le volume de l'étagère, la copie et la propre couverture du lecteur ne sont jamais visibles ensemble. - Un bouton natif par élément, pas par livre rendu. Une rangée en boucle rend des copies. Seule la copie réelle porte
button.bb-book__control, et les copies sontaria-hidden. Les faces sont de simples spans et le bouton est un élément séparé et invisible devant elles. Ne rendez pas les faces focusables et n'enveloppez pas les faces dans un<button>. - Gardez
touch-action: pan-ysur la surface de glissement. Avecnone, un doigt qui atterrit sur l'étagère ne peut plus faire défiler la page. - Pendant un glissement tactile, ignorez
lostpointercapturedes descendants. L'élément sous le doigt capture le pointeur implicitement, et lorsque la surface prend la capture, l'événement de perte de ce descendant remonte.gesture.tsvérifieevent.target === element. Gardez la vérification. - Les tapotements tactiles sur les volumes sont reconnus à partir des événements de pointeur, pas de
click. Après un balayage relâché à grande vitesse, les navigateurs peuvent avaler le clic du prochain tapotement.shelf.tsactive donc un volume surpointeruplorsque le toucher n'a jamais quitté un rayon de 10 px et a duré moins de 700 ms, et ignore le clic que le navigateur envoie pour ce même tapotement (les clics au clavier, qui ontdetail0, comptent toujours).gesture.tsconsomme égalementtouchmoveune fois qu'un glissement est verrouillé afin qu'aucun lancer de navigateur ne démarre. - Le lecteur construit le DOM avec
createElementettextContentuniquement. Il n'y a pas deinnerHTMLnulle part danssrc/reader, et un test au niveau de la source le vérifie. Les liens sont conservés uniquement pourhttp:ethttps:. - Le store n'accepte que les adresses
http:ethttps:et n'en récupère jamais aucune. L'application affiche un lien et tout texte que la personne a collé. N'ajoutez pas de récupération à distance pour rendre le lecteur « plus intelligent » : les navigateurs le bloquent pour la plupart des sites et un proxy modifie le modèle de sécurité. - Une importation ne remplace jamais, et rien qui vient de l'extérieur n'est appliqué sans un aperçu. Un fichier d'étagère ou un lien d'étagère provient d'un étranger : lisez-le avec
readShelfFileoudecodeShelfLink, affichezsummariseShelfFile, et appelezimportShelfuniquement après l'accord de la personne. Les positions de lecture, les marques de lecture et les notes privées ne vont jamais dans un fichier d'étagère. - Le mouvement réduit est son propre chemin, sans rien qui voyage.
reducedMotionsur le module de mouvement rend les déplacements instantanés et transforme l'ouverture et la fermeture en un fondu de 150 ms. Passez la préférence en direct (prefersReducedMotion), pas une valeur lue une fois. - Indiquez toujours la limite du lecteur à un endroit où les gens peuvent la voir. Un lien sans texte doit afficher « Ouvrir l'original » et un moyen en une étape d'ajouter du texte.
5. Vérifier le résultat
Exécutez ce qui s'applique. references/checks.md contient les commandes, les hooks DOM sur lesquels affirmer et des extraits Playwright pour les glissements réels à la souris et tactiles.
| A pris | Preuve minimale |
|---|---|
| n'importe quel module | npx tsc --noEmit propre, les *.test.ts du module passent sous Vitest avec happy-dom |
| shelf | les livres sont rendus, data-focus change sur setPose, exactement un .bb-book__control par élément, les touches fléchées émettent step |
| shelf + motion | sur une rangée assez longue pour boucler, un glissement réel de souris change data-focus-id et se termine par data-settled="1". Sur une rangée immobile, les touches fléchées le changent et un glissement ne le fait pas. Un glissement tactile vertical fait défiler la page |
| reader | Échap appelle onClose, un rechargement restaure scrollTop, un <script> dans le texte s'affiche sous forme de texte |
| store | une adresse javascript: est rejetée, l'export puis l'import font un aller-retour, un deuxième createStore sur le même stockage voit les données, une étagère survit à encodeShelfLink puis decodeShelfLink |
| les quatre | le test de vérification de configuration dans references/checks.md réussit. Ouvrez un volume, faites défiler, rechargez : le même volume est ouvert au même endroit. Fermez : le focus est de retour sur ce volume |
Rapportez ce que vous avez exécuté et ce que vous n'avez pas exécuté. Une construction qui passe la vérification de type n'a pas été démontrée comme glissable.
Looks
paper (défaut