Communitygithub.com

OthmanAdi/bookshelf

Adiciona uma estante 3D a um projeto web: uma fileira arrastável de lombadas de livros para um gerenciador de favoritos, uma lista de leitura posterior ou uma lista de leitura, onde links salvos se tornam livros que abrem em uma página de leitura. Fornece quatro módulos TypeScript sem dependências de runtime: um shelfe 3D em CSS (DOM, sem WebGL), um mecanismo de mola e arraste, um armazenamento local versionado de favoritos com importação e exportação, e um leitor de texto seguro que lembra a posição de leitura.

O que é bookshelf?

Esta skill implementa uma estante de livros 3D interativa para gerenciamento de favoritos e listas de leitura, composta por quatro módulos TypeScript independentes e sem dependências de runtime. O módulo `shelf` renderiza volumes 3D em CSS puro (sem WebGL) e reporta intenções do usuário como foco, abertura e passo. O módulo `motion` gerencia animações físicas com molas, arraste, deslize e encaixe, enquanto o `store` oferece um armazenamento versionado em localStorage com validação, migração, importação, exportação, busca e ordenação. O `reader` exibe texto salvo como nós de texto seguros, rastreando e restaurando a posição de leitura, tamanho da fonte e notas privadas. A skill é ideal para desenvolvedores que desejam integrar uma experiência de leitura imersiva e organizada em seus projetos web, suportando frameworks como React, Vue e Svelte através de uma API de montagem (`mount(element, options)`). Ela também inclui regras rígidas de acessibilidade e desempenho, como evitar quebras de contexto 3D e garantir que toques não interfiram na rolagem da página.

Funciona com✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/OthmanAdi/bookmark-bookshelf/tree/HEAD/.agents/skills/bookshelf

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Documentação

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óduloPastaFazNão sabe nada sobre
shelfsrc/shelfDesenha volumes para uma pose, reporta intenções (foco, abrir, passo, salto)tempo, gestos, armazenamento
motionsrc/motionMolas, arraste, deslize, encaixe, progresso de abertura e fechamentolivros, estrutura DOM
storesrc/storeBiblioteca versionada no localStorage: validação, migração, importação e exportação, busca, ordenação, uma estante como arquivo ou linkDOM, renderização
readersrc/readerMostra 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.

  1. Ao lado da skill. Se este arquivo estiver em <raiz>/.agents/skills/bookshelf/SKILL.md e <raiz>/src/shelf/index.ts existir, <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.

  2. 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.

  3. 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 release v0.2.0 em 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.

  4. Verifique antes de copiar. Na raiz da fonte:

    • package.json tem "name": "bookmark-bookshelf". Anote sua version.
    • 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 o types.ts de cada módulo antes de confiar nos arquivos de referência aqui: 0.1.0 não tem flight.ts, e seus contratos de shelf, store e reader diferem (veja o final de references/modules.md).

  5. 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 queremPeguePegue também
Uma fileira 3D de lombadas que podem arrastarshelf, motionsrc/styles/tokens.css
Uma estante com seu próprio tratamento de clique, sem arrasteshelftokens.css
Uma visualização de leitura com retomadareadertokens.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 estantestorenada
Arraste, deslize e encaixe para alguma outra fileiramotionnada
Uma estante que abre em um leitor, com seus próprios dadosshelf, motion, readertokens.css, as mesmas três regras de base.css
A composição completatodos os quatro, src/app/bookshelf.ts, src/app/bookshelf.css, src/app/flight.tssrc/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

  1. Copie cada pasta escolhida de src/ inalterada, incluindo testes (*.test.ts), para a árvore de fonte do anfitrião.
  2. Copie src/styles/tokens.css e 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 em src/styles/base.css. Em um projeto vazio, copie esse arquivo também (ele importa tokens.css e fonts.css, então remova a importação de fontes se você pular o passo 3), ou defina background: var(--bb-paper); color: var(--bb-ink); font-family: var(--bb-font-serif) no body você mesmo.
  3. Fontes são opcionais. src/styles/fonts.css inclui Newsreader e IBM Plex Mono (SIL OFL 1.1) e precisa de duas dependências de desenvolvimento: @fontsource-variable/newsreader e @fontsource/ibm-plex-mono. Se você as enviar, envie os textos de licença em public/licenses/ também. Sem elas, remova a linha @import './fonts.css' de base.css e as pilhas de tokens voltam para fontes do sistema.
  4. shelf.ts e reader.ts importam 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.css e variants.css para o shelf, reader.css e reader-text.css para o reader.
  5. TypeScript: o código é escrito para strict, noUncheckedIndexedAccess e verbatimModuleSyntax, com moduleResolution: "bundler". Ele compila sob configurações mais flexíveis também.
  6. 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.md tem 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__row e a face de um livro pode definir overflow, filter, clip-path, mask, contain: paint ou opacity abaixo 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 setPose em um loop de frames de propriedade do módulo motion. Adicionar transições CSS a .bb-book luta contra esse loop. Efeitos de hover vão em .bb-book__body.
  • A estante não voa a capa. Enquanto pose.open está 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 por transform, de shelf.coverPlacement() para reader.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ão aria-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-y na superfície de arraste. Com none, um dedo que pousa na estante não pode mais rolar a página.
  • Durante um arraste por toque, ignore lostpointercapture de 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.ts verifica event.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.ts portanto ativa um volume em pointerup quando 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êm detail 0, sempre contam). gesture.ts também consome touchmove uma vez que um arraste é travado para que nenhum deslize do navegador comece.
  • O reader constrói DOM apenas com createElement e textContent. Não há innerHTML em nenhum lugar em src/reader, e um teste de nível de fonte o impõe. Links são mantidos apenas para http: e https:.
  • O store aceita apenas endereços http: e https: 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 readShelfFile ou decodeShelfLink, mostre summariseShelfFile, e chame importShelf apenas 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. reducedMotion no 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.

PegouProva mínima
qualquer módulonpx tsc --noEmit limpo, os próprios *.test.ts do módulo passam sob Vitest com happy-dom
shelflivros renderizam, data-focus muda em setPose, exatamente um .bb-book__control por item, teclas de seta emitem step
shelf + motionem 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
readerEscape chama onClose, um recarregamento restaura scrollTop, um <script> no texto aparece como texto
storeum 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 quatroa 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)

Habilidades Relacionadas