Estante de Favoritos (Bookmark Bookshelf)
Quatro pequenos módulos TypeScript sem dependências de runtime. Cada um funciona sozinho. Juntos, formam uma estante de links salvos que você arrasta, abre e lê. Esta skill descreve a versão 0.2.0.
| Módulo | Pasta | Faz | Não sabe nada sobre |
|---|---|---|---|
| shelf | src/shelf | Desenha volumes para uma pose, reporta intenções (foco, abrir, passo, salto) | tempo, gestos, armazenamento |
| motion | src/motion | Molas, arraste, deslize, encaixe, progresso de abertura e fechamento | livros, estrutura DOM |
| store | src/store | Biblioteca versionada no localStorage: validação, migração, importação e exportação, busca, ordenação, uma estante como arquivo ou link | DOM, renderização |
| reader | src/reader | Mostra texto salvo como nós de texto, rastreia e restaura a posição, tamanho do texto, notas privadas, "Marcar como lido" | estantes, armazenamento |
Os módulos não estão dentro desta pasta de skill. Eles estão em um checkout do repositório Bookmark Bookshelf, e a primeira tarefa é encontrá-lo. Copie as pastas dos módulos de lá e não as redigite.
0. Encontre a raiz da fonte
Tente as rotas nesta ordem e pare na primeira que funcionar.
-
Ao lado da skill. Se este arquivo estiver em
<raiz>/.agents/skills/bookshelf/SKILL.mde<raiz>/src/shelf/index.tsexistir,<raiz>é a raiz da fonte. Este é o caso quando a skill é usada no local, e quando foi instalada como um plugin que mantém o repositório inteiro. -
Um caminho que a pessoa fornecer. Se a skill foi copiada para outro lugar (a pasta de skills de um agente), a pessoa pode já ter um checkout. Se a solicitação contiver um caminho, use-o como raiz da fonte. Se não contiver, peça um, e ofereça a rota 3 na mesma pergunta.
-
Um clone raso, somente após a pessoa concordar. Pergunte primeiro, e diga claramente o que acontecerá: o repositório público
https://github.com/OthmanAdi/bookmark-bookshelfé clonado na tag de releasev0.2.0em uma nova pasta temporária, e nada mais é baixado. Não baixe nada antes da pessoa dizer sim. Após um sim, execute:git clone --depth 1 --branch v0.2.0 https://github.com/OthmanAdi/bookmark-bookshelf <pasta temporária>A pasta temporária é um diretório novo e vazio fora do projeto (o diretório temporário do sistema é adequado). Informe a pessoa onde ela está. Ela é a raiz da fonte. Copie de lá, nunca edite dentro dela, e diga no final que ela pode ser deletada. Se o clone falhar (sem rede, sem git, tag ausente), diga e volte para a rota 2.
-
Verifique antes de copiar. Na raiz da fonte:
package.jsontem"name": "bookmark-bookshelf". Anote suaversion.- Estes marcadores existem:
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. - Se for um checkout git, anote
git -C <raiz> rev-parse --short HEAD.
Se um marcador estiver faltando, pare e diga qual. Esta skill descreve a versão
0.2.x. Para outra versão, leia otypes.tsde cada módulo antes de confiar nos arquivos de referência aqui: 0.1.0 não temflight.ts, e seus contratos de shelf, store e reader diferem (veja o final dereferences/modules.md). -
Informe o caminho da fonte, versão e commit de onde você copiou, e se foi um clone que você fez, para que uma atualização posterior possa ser comparada.
Cada caminho abaixo que começa com src/, examples/ ou public/ é relativo a essa raiz da fonte.
1. Decida o escopo primeiro
Pergunte o que a pessoa precisa, então pegue apenas isso.
| Eles querem | Pegue | Pegue também |
|---|---|---|
| Uma fileira 3D de lombadas que podem arrastar | shelf, motion | src/styles/tokens.css |
| Uma estante com seu próprio tratamento de clique, sem arraste | shelf | tokens.css |
| Uma visualização de leitura com retomada | reader | tokens.css, as regras .bb-button, .bb-label e .bb-visually-hidden de src/styles/base.css |
| Uma biblioteca de favoritos sem interface: importação, exportação, busca, compartilhar uma estante | store | nada |
| Arraste, deslize e encaixe para alguma outra fileira | motion | nada |
| Uma estante que abre em um leitor, com seus próprios dados | shelf, motion, reader | tokens.css, as mesmas três regras de base.css |
| A composição completa | todos os quatro, src/app/bookshelf.ts, src/app/bookshelf.css, src/app/flight.ts | src/styles/*, src/data/* se os links de exemplo forem desejados |
Módulos nunca importam uns aos outros. Se uma importação entre duas pastas de módulo aparecer, a integração está errada: o ponto de encontro pertence ao próprio arquivo de composição do anfitrião (veja src/app/bookshelf.ts).
A composição completa são sete coisas, e a lista é fácil de ficar com uma faltando: as quatro pastas de módulo, src/app/bookshelf.ts, sua folha de estilo src/app/bookshelf.css, e src/app/flight.ts, que carrega a capa entre a estante e a página. O script importa os outros dois, então uma compilação sem eles falha com um erro de arquivo ausente. Copie os três arquivos juntos, em uma pasta que fique ao lado das pastas de módulo.
O aplicativo ao redor da composição (a barra e seus menus, os diálogos para adicionar links, trazer muitos e compartilhar uma estante, a visualização de lista) é o resto de src/app/ junto com app.html. Esses arquivos dependem uns dos outros e de ids de elementos em app.html. Pegue-os como um todo e adapte, ou construa sua própria interface sobre as funções do store, que references/composition.md mostra.
2. Traga os arquivos
- Copie cada pasta escolhida de
src/inalterada, incluindo testes (*.test.ts), para a árvore de fonte do anfitrião. - Copie
src/styles/tokens.csse carregue-o uma vez. Os módulos leem propriedades customizadas--bb-*para cor, tipo, espaçamento e temporização. Para reestilizar, sobrescreva tokens e não edite o CSS do módulo. Tokens apenas definem valores: o fundo da página, fonte do corpo e resets vivem emsrc/styles/base.css. Em um projeto vazio, copie esse arquivo também (ele importatokens.cssefonts.css, então remova a importação de fontes se você pular o passo 3), ou definabackground: var(--bb-paper); color: var(--bb-ink); font-family: var(--bb-font-serif)nobodyvocê mesmo. - Fontes são opcionais.
src/styles/fonts.cssinclui Newsreader e IBM Plex Mono (SIL OFL 1.1) e precisa de duas dependências de desenvolvimento:@fontsource-variable/newsreadere@fontsource/ibm-plex-mono. Se você as enviar, envie os textos de licença empublic/licenses/também. Sem elas, remova a linha@import './fonts.css'debase.csse as pilhas de tokens voltam para fontes do sistema. shelf.tsereader.tsimportam seu CSS (import './shelf.css'). Isso precisa de um bundler que lide com importações de CSS (Vite, webpack, Parcel, esbuild com um loader CSS). Sem um, delete essas linhas de importação e vincule os arquivos CSS da página:shelf.css,cover.cssevariants.csspara o shelf,reader.cssereader-text.csspara o reader.- TypeScript: o código é escrito para
strict,noUncheckedIndexedAccesseverbatimModuleSyntax, commoduleResolution: "bundler". Ele compila sob configurações mais flexíveis também. - Ferramentas para um projeto vazio:
npm i -D vite typescript vitest happy-dom @playwright/test, Node^22.12.0 || ^24.0.0 || >=26.0.0.references/checks.mdtem as configurações mínimas de Vitest e Playwright.
Anfitriões de framework (React, Vue, Svelte): cada módulo é mount(element, options) retornando um handle com destroy(). Monte no gancho "após a primeira renderização" do framework e destrua em sua limpeza. references/composition.md tem uma função simples para encapsular.
3. Conecte
Leia references/modules.md para os contratos (entradas, saídas, eventos) e references/composition.md para a fiação pronta para copiar de cada combinação. A mais curta útil, shelf mais motion:
import { createShelfMotion, prefersReducedMotion } from './motion';
import { mountShelf } from './shelf';
const motion = createShelfMotion({ count: items.length, reducedMotion: prefersReducedMotion });
const shelf = mountShelf(host, {
items,
label: 'Minha estante',
onLoopChange: (loop) => motion.setLoop(loop), // a estante decide se a fileira faz 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'; // permite que testes esperem o repouso
});
motion.attach(shelf.surface, {
pxPerItem: () => shelf.pitch(),
extent: () => shelf.surface.clientWidth,
enabled: () => !shelf.fixed, // uma fileira curta fica parada, então um arraste não deve movê-la
});
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 é um array de { id, title, source?, author?, cloth?, pattern?, seed?, progress?, thickness?, stature?, outline?, note?, done? }. Versões executáveis das combinações estão em examples/.
O loop é decidido pela estante. Uma fileira faz loop apenas quando seus volumes preenchem pelo menos quatro quintos da cena. Uma fileira mais curta fica parada no meio entre dois marcadores de final (shelf.fixed): as setas, as teclas de seta e um clique em uma lombada escolhem o volume apresentado, e um arraste não faz nada. Informe a pessoa qual dos dois eles verão com seu número de itens, porque oito títulos em uma tela de desktop ficam parados.
4. Regras que mantêm o funcionamento
Estas são as coisas que quebram quando alteradas. Cada uma tem um motivo.
- Nada entre
.bb-shelf__rowe a face de um livro pode definiroverflow,filter,clip-path,mask,contain: paintouopacityabaixo de 1. Qualquer um deles achata o contexto 3D e os livros colapsam em um plano. Desbote a cena (o elemento de perspectiva) em vez disso. - A estante desenha a pose que lhe é dada. O movimento ao longo da fileira e a abertura vêm de
setPoseem um loop de frames de propriedade do módulo motion. Adicionar transições CSS a.bb-bookluta contra esse loop. Efeitos de hover vão em.bb-book__body. - A estante não voa a capa. Enquanto
pose.openestá acima de 0, ela esconde o volume central e afasta seus vizinhos. Uma capa viajante é trabalho do anfitrião (src/app/flight.ts): uma cópia em uma camada fixa, movida apenas portransform, deshelf.coverPlacement()parareader.flightTarget(). O volume da estante, a cópia e a capa própria do reader nunca são visíveis juntos. - Um botão nativo por item, não por livro renderizado. Uma fileira em loop renderiza cópias. Apenas a cópia real carrega
button.bb-book__control, e as cópias sãoaria-hidden. As faces são spans simples e o botão é um elemento separado e invisível na frente delas. Não torne as faces focáveis e não envolva as faces em um<button>. - Mantenha
touch-action: pan-yna superfície de arraste. Comnone, um dedo que pousa na estante não pode mais rolar a página. - Durante um arraste por toque, ignore
lostpointercapturede descendentes. O elemento sob o dedo captura o ponteiro implicitamente, e quando a superfície assume a captura, o evento de perda desse descendente borbulha para cima.gesture.tsverificaevent.target === element. Mantenha a verificação. - Toques em volumes são reconhecidos a partir de eventos de ponteiro, não de
click. Após um deslize liberado em velocidade, navegadores podem engolir o click do próximo toque.shelf.tsportanto ativa um volume empointerupquando o toque nunca saiu de um raio de 10 px e durou menos de 700 ms, e ignora o click que o navegador envia para esse mesmo toque (cliques de teclado, que têmdetail0, sempre contam).gesture.tstambém consometouchmoveuma vez que um arraste é travado para que nenhum deslize do navegador comece. - O reader constrói DOM apenas com
createElementetextContent. Não háinnerHTMLem nenhum lugar emsrc/reader, e um teste de nível de fonte o impõe. Links são mantidos apenas parahttp:ehttps:. - O store aceita apenas endereços
http:ehttps:e nunca busca um. O app mostra um link e qualquer texto que a pessoa colou. Não adicione busca remota para tornar o reader "mais inteligente": navegadores bloqueiam para a maioria dos sites e um proxy muda o modelo de segurança. - Uma importação nunca sobrescreve, e nada que chega de fora é aplicado sem uma prévia. Um arquivo de estante ou link de estante vem de um estranho: leia com
readShelfFileoudecodeShelfLink, mostresummariseShelfFile, e chameimportShelfapenas após a pessoa concordar. Posições de leitura, marcas de leitura e notas privadas nunca vão para um arquivo de estante. - Movimento reduzido é seu próprio caminho, sem nada viajando.
reducedMotionno módulo motion torna os movimentos instantâneos e transforma abertura e fechamento em um fade de 150 ms. Passe a preferência ativa (prefersReducedMotion), não um valor lido uma vez. - Sempre indique o limite do reader onde as pessoas possam vê-lo. Um link sem texto deve mostrar "Abrir original" e uma maneira de uma etapa para adicionar texto.
5. Verifique o resultado
Execute o que se aplica. references/checks.md tem os comandos, os hooks de DOM para afirmar e trechos de Playwright para arrastes reais de mouse e toque.
| Pegou | Prova mínima |
|---|---|
| qualquer módulo | npx tsc --noEmit limpo, os próprios *.test.ts do módulo passam sob Vitest com happy-dom |
| shelf | livros renderizam, data-focus muda em setPose, exatamente um .bb-book__control por item, teclas de seta emitem step |
| shelf + motion | em uma fileira longa o suficiente para fazer loop, um arraste real de mouse muda data-focus-id e termina com data-settled="1". Em uma fileira parada, as teclas de seta mudam e um arraste não muda. Um arraste de toque vertical rola a página |
| reader | Escape chama onClose, um recarregamento restaura scrollTop, um <script> no texto aparece como texto |
| store | um endereço javascript: é rejeitado, exportação seguida de importação faz ida e volta, um segundo createStore no mesmo armazenamento vê os dados, uma estante sobrevive a encodeShelfLink seguido de decodeShelfLink |
| todos os quatro | a verificação de fumaça da configuração em references/checks.md passa. Abra um volume, role, recarregue: o mesmo volume está aberto no mesmo lugar. Feche: o foco está de volta naquele volume |
Relate o que você executou e o que não executou. Uma compilação que passa na verificação de tipo não foi mostrada para arrastar.
Aparência
paper (padrão)