Estantería de Marcadores (Bookmark Bookshelf)
Cuatro pequeños módulos TypeScript sin dependencias de ejecución. Cada uno funciona por sí solo. Juntos forman una estantería de enlaces guardados que puedes arrastrar, abrir y leer. Este skill describe la versión 0.2.0.
| Módulo | Carpeta | Función | No sabe nada de |
|---|---|---|---|
| shelf | src/shelf | Dibuja volúmenes para una pose, reporta intenciones (foco, abrir, paso, salto) | tiempo, gestos, almacenamiento |
| motion | src/motion | Resortes, arrastre, impulso, ajuste, progreso de apertura y cierre | libros, estructura DOM |
| store | src/store | Biblioteca versionada en almacenamiento local: validación, migración, importación y exportación, búsqueda, ordenación, una estantería como archivo o enlace | DOM, renderizado |
| reader | src/reader | Muestra texto guardado como nodos de texto, rastrea y restaura la posición, tamaño de texto, notas privadas, "Marcar como leído" | estanterías, almacenamiento |
Los módulos no están dentro de esta carpeta de skill. Viven en un checkout del repositorio Bookmark Bookshelf, y la primera tarea es encontrarlo. Copia las carpetas de los módulos desde allí y no las vuelvas a escribir.
0. Encontrar la raíz del código fuente
Prueba las rutas en este orden y detente en la primera que funcione.
-
Junto al skill. Si este archivo está en
<raíz>/.agents/skills/bookshelf/SKILL.mdy<raíz>/src/shelf/index.tsexiste,<raíz>es la raíz del código fuente. Este es el caso cuando el skill se usa en su lugar, y cuando se instaló como un plugin que conserva todo el repositorio. -
Una ruta que proporcione la persona. Si el skill se copió en otro lugar (la carpeta de skills de un agente), la persona puede tener ya un checkout. Si la solicitud contiene una ruta, úsala como raíz del código fuente. Si no, pide una, y ofrece la ruta 3 en la misma pregunta.
-
Un clon superficial, solo después de que la persona acepte. Pregunta primero, y di claramente lo que sucederá: el repositorio público
https://github.com/OthmanAdi/bookmark-bookshelfse clona en la etiqueta de versiónv0.2.0en una nueva carpeta temporal, y no se descarga nada más. No obtengas nada antes de que la persona diga que sí. Después de un sí, ejecuta:git clone --depth 1 --branch v0.2.0 https://github.com/OthmanAdi/bookmark-bookshelf <carpeta temporal>La carpeta temporal es un directorio nuevo y vacío fuera del proyecto (el directorio temporal del sistema está bien). Indica a la persona dónde está. Es la raíz del código fuente. Copia desde allí, nunca edites dentro de ella, y di al final que se puede eliminar. Si el clon falla (sin red, sin git, etiqueta faltante), dilo y vuelve a la ruta 2.
-
Verifica antes de copiar. En la raíz del código fuente:
package.jsontiene"name": "bookmark-bookshelf". Anota suversion.- Estos marcadores existen:
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. - Si es un checkout de git, anota
git -C <raíz> rev-parse --short HEAD.
Si falta un marcador, detente y di cuál. Este skill describe la versión
0.2.x. Para otra versión, lee eltypes.tsde cada módulo antes de confiar en los archivos de referencia aquí: 0.1.0 no tieneflight.ts, y sus contratos de estantería, almacén y lector difieren (ver el final dereferences/modules.md). -
Informa la ruta de origen, versión y commit desde los que copiaste, y si fue un clon que hiciste, para que una actualización posterior pueda compararse.
Cada ruta a continuación que comienza con src/, examples/ o public/ es relativa a esa raíz del código fuente.
1. Decidir el alcance primero
Pregunta qué necesita la persona, luego toma solo eso.
| Ellos quieren | Toma | También toma |
|---|---|---|
| Una fila 3D de lomos que se pueda arrastrar | shelf, motion | src/styles/tokens.css |
| Una estantería con su propio manejo de clics, sin arrastre | shelf | tokens.css |
| Una vista de lectura con reanudación | reader | tokens.css, las reglas .bb-button, .bb-label y .bb-visually-hidden de src/styles/base.css |
| Una biblioteca de marcadores sin interfaz de usuario: importar, exportar, buscar, compartir una estantería | store | nada |
| Arrastre, impulso y ajuste para alguna otra fila | motion | nada |
| Una estantería que se abre en un lector, con sus propios datos | shelf, motion, reader | tokens.css, las mismas tres reglas de base.css |
| Toda la composición | los cuatro, src/app/bookshelf.ts, src/app/bookshelf.css, src/app/flight.ts | src/styles/*, src/data/* si se desean los enlaces de ejemplo |
Los módulos nunca se importan entre sí. Si aparece una importación entre dos carpetas de módulos, la integración es incorrecta: el punto de encuentro pertenece al propio archivo de composición del anfitrión (ver src/app/bookshelf.ts).
Toda la composición son siete cosas, y es fácil que falte una: las cuatro carpetas de módulos, src/app/bookshelf.ts, su hoja de estilo src/app/bookshelf.css, y src/app/flight.ts, que transporta la portada entre la estantería y la página. El script importa los otros dos, por lo que una compilación sin ellos falla con un error de archivo faltante. Copia los tres archivos juntos, en una carpeta que esté junto a las carpetas de módulos.
La aplicación que rodea la composición (la barra y sus menús, los diálogos para agregar enlaces, importar muchos y compartir una estantería, la vista de lista) es el resto de src/app/ junto con app.html. Esos archivos dependen unos de otros y de los ids de elementos en app.html. Tómalos en su totalidad y adáptalos, o construye tu propia interfaz sobre las funciones del almacén, como se muestra en references/composition.md.
2. Incorporar los archivos
- Copia cada carpeta elegida de
src/sin cambios, incluidas las pruebas (*.test.ts), en el árbol fuente del anfitrión. - Copia
src/styles/tokens.cssy cárgalo una vez. Los módulos leen las propiedades personalizadas--bb-*para color, tipo, espaciado y temporización. Para cambiar el estilo, anula los tokens y no edites el CSS de los módulos. Los tokens solo definen valores: el fondo de página, la fuente del cuerpo y los resets viven ensrc/styles/base.css. En un proyecto vacío, copia también ese archivo (importatokens.cssyfonts.css, así que elimina la importación de fuentes si omites el paso 3), o establecebackground: var(--bb-paper); color: var(--bb-ink); font-family: var(--bb-font-serif)enbodytú mismo. - Las fuentes son opcionales.
src/styles/fonts.cssincluye Newsreader e IBM Plex Mono (SIL OFL 1.1) y necesita dos dependencias de desarrollo:@fontsource-variable/newsreadery@fontsource/ibm-plex-mono. Si las incluyes, incluye también los textos de licencia enpublic/licenses/. Sin ellas, elimina la línea@import './fonts.css'debase.cssy las pilas de tokens recurren a las fuentes del sistema. shelf.tsyreader.tsimportan su CSS (import './shelf.css'). Esto necesita un empaquetador que maneje importaciones de CSS (Vite, webpack, Parcel, esbuild con un cargador CSS). Sin uno, elimina esas líneas de importación y enlaza los archivos CSS desde la página:shelf.css,cover.cssyvariants.csspara la estantería,reader.cssyreader-text.csspara el lector.- TypeScript: el código está escrito para
strict,noUncheckedIndexedAccessyverbatimModuleSyntax, conmoduleResolution: "bundler". También compila con configuraciones más flexibles. - Herramientas para un proyecto vacío:
npm i -D vite typescript vitest happy-dom @playwright/test, Node^22.12.0 || ^24.0.0 || >=26.0.0.references/checks.mdtiene la configuración mínima de Vitest y Playwright.
Anfitriones de frameworks (React, Vue, Svelte): cada módulo es mount(element, options) que devuelve un manejador con destroy(). Monta en el hook "después del primer renderizado" del framework y destruye en su limpieza. references/composition.md tiene una función simple para envolver.
3. Cablear
Lee references/modules.md para los contratos (entradas, salidas, eventos) y references/composition.md para el cableado listo para copiar de cada combinación. La más corta útil, estantería más movimiento:
import { createShelfMotion, prefersReducedMotion } from './motion';
import { mountShelf } from './shelf';
const motion = createShelfMotion({ count: items.length, reducedMotion: prefersReducedMotion });
const shelf = mountShelf(host, {
items,
label: 'Mi estantería',
onLoopChange: (bucle) => motion.setLoop(bucle), // la estantería decide si la fila se repite
});
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'; // permite que las pruebas esperen al reposo
});
motion.attach(shelf.surface, {
pxPerItem: () => shelf.pitch(),
extent: () => shelf.surface.clientWidth,
enabled: () => !shelf.fixed, // una fila corta permanece quieta, por lo que un arrastre no debe moverla
});
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 es un array de { id, title, source?, author?, cloth?, pattern?, seed?, progress?, thickness?, stature?, outline?, note?, done? }. Las versiones ejecutables de las combinaciones están en examples/.
El bucle lo decide la estantería. Una fila se repite solo cuando sus volúmenes llenan al menos cuatro quintos de la escena. Una fila más corta permanece quieta en el medio entre dos guardalibros (shelf.fixed): las flechas, las teclas de flecha y un clic en un lomo eligen el volumen presentado, y un arrastre no hace nada. Indica a la persona cuál de los dos verá con su número de elementos, porque ocho títulos en una pantalla de escritorio permanecen quietos.
4. Reglas que lo mantienen funcionando
Estas son las cosas que se rompen cuando se cambian. Cada una tiene una razón.
- Nada entre
.bb-shelf__rowy una cara de libro puede estableceroverflow,filter,clip-path,mask,contain: paintuopacitypor debajo de 1. Cualquiera de ellos aplana el contexto 3D y los libros se colapsan en un plano. Atenúa la escena (el elemento de perspectiva) en su lugar. - La estantería dibuja la pose que se le da. El movimiento a lo largo de la fila y la apertura provienen de
setPoseen un bucle de fotogramas propiedad del módulo de movimiento. Agregar transiciones CSS a.bb-booklucha contra ese bucle. Los efectos de hover van en.bb-book__body. - La estantería no vuela la portada. Mientras
pose.openestá por encima de 0, oculta el volumen centrado y separa a sus vecinos. Una portada viajera es trabajo del anfitrión (src/app/flight.ts): una copia en una capa fija, movida solo portransform, desdeshelf.coverPlacement()hastareader.flightTarget(). El volumen de la estantería, la copia y la propia portada del lector nunca son visibles juntos. - Un botón nativo por elemento, no por libro renderizado. Una fila en bucle renderiza copias. Solo la copia real lleva
button.bb-book__control, y las copias sonaria-hidden. Las caras son simples spans y el botón es un elemento separado e invisible delante de ellas. No hagas que las caras sean enfocables y no envuelvas las caras en un<button>. - Mantén
touch-action: pan-yen la superficie de arrastre. Connone, un dedo que aterriza en la estantería ya no puede desplazar la página. - Durante un arrastre táctil, ignora
lostpointercapturede los descendientes. El elemento bajo el dedo captura el puntero implícitamente, y cuando la superficie toma la captura, el evento de pérdida de ese descendiente burbujea hacia arriba.gesture.tsverificaevent.target === element. Mantén la verificación. - Los toques táctiles en los volúmenes se reconocen a partir de eventos de puntero, no de
click. Después de un deslizamiento soltado a velocidad, los navegadores pueden tragarse el clic del siguiente toque.shelf.tspor lo tanto activa un volumen enpointerupcuando el toque nunca salió de un radio de 10 px y duró menos de 700 ms, y omite el clic que el navegador envía para ese mismo toque (los clics de teclado, que tienendetail0, siempre cuentan).gesture.tstambién consumetouchmoveuna vez que un arrastre está bloqueado para que no comience ningún desplazamiento del navegador. - El lector construye DOM solo con
createElementytextContent. No hayinnerHTMLen ninguna parte desrc/reader, y una prueba a nivel de código fuente lo exige. Los enlaces se mantienen solo parahttp:yhttps:. - El almacén acepta solo direcciones
http:yhttps:y nunca obtiene una. La aplicación muestra un enlace y cualquier texto que la persona haya pegado. No agregues obtención remota para hacer el lector "más inteligente": los navegadores lo bloquean para la mayoría de los sitios y un proxy cambia el modelo de seguridad. - Una importación nunca sobrescribe, y nada que llegue desde fuera se aplica sin una vista previa. Un archivo de estantería o enlace de estantería proviene de un extraño: léelo con
readShelfFileodecodeShelfLink, muestrasummariseShelfFile, y llama aimportShelfsolo después de que la persona acepte. Las posiciones de lectura, las marcas de leído y las notas privadas nunca van a un archivo de estantería. - El movimiento reducido es su propio camino, sin nada que viaje.
reducedMotionen el módulo de movimiento hace que los movimientos sean instantáneos y convierte la apertura y el cierre en un desvanecimiento de 150 ms. Pasa la preferencia en vivo (prefersReducedMotion), no un valor leído una vez. - Indica siempre el límite del lector donde la gente pueda verlo. Un enlace sin texto debe mostrar "Abrir original" y una forma de un solo paso para agregar texto.
5. Verificar el resultado
Ejecuta lo que corresponda. references/checks.md tiene los comandos, los hooks DOM para afirmar y fragmentos de Playwright para arrastres reales con ratón y táctil.
| Tomó | Mínima prueba |
|---|---|
| cualquier módulo | npx tsc --noEmit limpio, las pruebas *.test.ts del módulo pasan con Vitest y happy-dom |
| estantería | los libros se renderizan, data-focus cambia en setPose, exactamente un .bb-book__control por elemento, las teclas de flecha emiten step |
| estantería + movimiento | en una fila lo suficientemente larga para bucle, un arrastre real con ratón cambia data-focus-id y termina con data-settled="1". En una fila quieta, las teclas de flecha lo cambian y un arrastre no. Un arrastre táctil vertical desplaza la página |
| lector | Escape llama a onClose, una recarga restaura scrollTop, un <script> en el texto se muestra como texto |
| almacén | una dirección javascript: es rechazada, exportar luego importar da la vuelta completa, un segundo createStore en el mismo almacenamiento ve los datos, una estantería sobrevive a encodeShelfLink luego decodeShelfLink |
| los cuatro | la prueba de humo de configuración en references/checks.md pasa. Abre un volumen, desplázate, recarga: el mismo volumen está abierto en el mismo lugar. Cierra: el foco vuelve a ese volumen |
Informa lo que ejecutaste y lo que no. Una compilación que pasa la verificación de tipos no ha demostrado poder arrastrarse.
Apariencia
paper (defau...