From 7bbdfd9dafe92e21d52f60ee6f51e59bd53d091f Mon Sep 17 00:00:00 2001 From: Evandro Fonseca Junior Date: Sun, 30 Aug 2026 23:19:09 -0300 Subject: [PATCH] docs: spec notebook story interativo --- .../specs/2026-08-30-notebook-story-design.md | 616 ++++++++++++++++++ 1 file changed, 616 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-30-notebook-story-design.md diff --git a/docs/superpowers/specs/2026-08-30-notebook-story-design.md b/docs/superpowers/specs/2026-08-30-notebook-story-design.md new file mode 100644 index 0000000..2307b78 --- /dev/null +++ b/docs/superpowers/specs/2026-08-30-notebook-story-design.md @@ -0,0 +1,616 @@ +# NotebookStory — design e especificação de implementação + +> O portfólio deixa de mostrar telas como uma faixa que passa diante do visitante e passa a revelar sistemas como uma sessão de trabalho: abrir, navegar, trocar de projeto, fechar. + +## Status + +- **Estado:** direção aprovada para implementação. +- **Escopo:** substituir o palco 3D do celular e o `PrintReel` horizontal por uma única experiência de notebook. +- **Interação:** L3 — pin, scrub reversível, objeto 3D, cursor coreografado e tabs acessíveis. +- **Implementação:** ainda não iniciada. + +## Decisões fechadas + +1. O bloco de história terá um único dispositivo e um único Canvas WebGL. A hero atual não ganha outro renderer por causa desta mudança. +2. O notebook usará um modelo 3D CC0, com abertura controlada por rig próprio. +3. A tela não usará iframe nem dependerá de rede para renderizar a narrativa. +4. A interface da tela será determinística, usando screenshots reais dos projetos já presentes no repositório. +5. O cursor será coreografado pelo scroll; as tabs e o CTA continuarão sendo controles DOM reais. +6. O `PrintReel` será removido depois da migração, sem alias ou caminho paralelo. +7. O scroll continuará sendo o controle de play e rewind. Não será introduzido Lenis. + +## Objetivo e não escopo + +### Objetivo + +Criar uma seção de prova visual dos projetos em que o visitante percebe um notebook físico abrindo, vê uma interface de navegador, acompanha um cursor trocando de aba entre projetos reais e observa o dispositivo fechar antes de continuar para a seção `About`. + +A experiência deve comunicar três coisas sem depender de texto longo: + +- os projetos são sistemas reais, não apenas imagens de portfólio; +- cada projeto tem uma interface e um contexto próprios; +- a engenharia é apresentada como uma sequência de decisões operacionais. + +### Não escopo + +- Navegar sites reais dentro do notebook. +- Reproduzir autenticação, formulários ou estados internos dos deployments. +- Criar um desktop completo ou um sistema operacional fictício. +- Adicionar áudio, vídeo, partículas pesadas ou um cursor global novo. +- Manter o celular como uma segunda narrativa. +- Redesenhar as seções `About`, `Skills`, `Experience`, `Projects` ou `Contact`. + +## 1. Visual Theme & Atmosphere + +**Style:** Dark Editorial / Systems Workbench. + +**Keywords:** bancada escura, metal fosco, browser chrome, prova operacional, cursor preciso, verde ácido, tipografia técnica, silêncio entre ações. + +**Tone:** cinematográfico e técnico, mas sóbrio — não gaming, não cyberpunk, não demonstração de template. + +**Feel:** uma câmera observa uma máquina de trabalho real sendo preparada, usada e fechada no fim do expediente. + +**Interaction Tier:** L3 — experiência imersiva com uma cena pinada, timeline scrubbed, WebGL e interação DOM. + +**Dependencies:** React/R3F/Three/Drei já existentes + GSAP/ScrollTrigger já existentes. Não adicionar dependências. + +### Direção de composição + +- Fundo: `var(--ink)` com grade discreta e halo radial em `var(--slash-dim)`. +- Texto narrativo: lado esquerdo em desktop, alinhado à mesma margem do restante da home. +- Notebook: lado direito em desktop, ocupando o foco visual sem encobrir o texto. +- Tela: navegador escuro, borda fina, barra de abas legível e screenshot com contraste suficiente. +- Acento: `var(--slash)` somente em estado ativo, progresso, cursor e CTA. +- O modelo não deve parecer um produto anunciado. Deve parecer uma ferramenta em uso. + +### Estrutura narrativa + +1. **Preparar:** notebook fechado repousa sobre a bancada. +2. **Abrir:** a tampa sobe pela dobradiça; o usuário entende imediatamente a mecânica. +3. **Ligar:** o browser chrome aparece e a tela acende. +4. **Explorar:** o cursor entra, clica nas tabs e troca o screenshot do projeto. +5. **Concluir:** a última tela recebe uma pausa para leitura e um CTA real. +6. **Encerrar:** cursor sai, tela reduz a luminosidade, notebook fecha e a seção libera o fluxo normal. + +## 2. Color Palette & Roles + +A implementação reutiliza os tokens existentes em `app/globals.css`. Os nomes abaixo são o contrato visual do novo palco; novos hexadecimais não devem ser introduzidos em componentes. + +```css +:root { + /* Backgrounds */ + --notebook-bg: var(--ink); + --notebook-surface: #111214; + --notebook-surface-alt: rgba(255, 255, 255, 0.04); + --notebook-surface-hover: rgba(255, 255, 255, 0.08); + + /* Borders */ + --notebook-border: rgba(255, 255, 255, 0.12); + --notebook-border-hover: rgba(204, 255, 0, 0.62); + + /* Text */ + --notebook-text: var(--paper); + --notebook-text-secondary: rgba(244, 242, 237, 0.72); + --notebook-text-tertiary: rgba(244, 242, 237, 0.48); + + /* Accent */ + --notebook-accent: var(--slash); + --notebook-accent-hover: var(--white); + + /* RGB variants */ + --notebook-bg-rgb: 10 10 10; + --notebook-paper-rgb: 244 242 237; + --notebook-accent-rgb: 204 255 0; + + /* Semantic */ + --notebook-success: var(--slash); + --notebook-error: #ff6b6b; + --notebook-warning: #f5c451; +} +``` + +### Regras de cor + +- Todo CSS novo usa tokens; nenhum componente recebe hex solto. +- O verde ácido marca apenas ação, estado ativo, foco, progresso ou informação de produção. +- Texto de descrição não usa o verde como cor principal. +- Estados de erro e indisponibilidade não serão simulados dentro da narrativa; projetos não aprovados são removidos da tablist. +- Materiais R3F usam um mapa de cores central que replica os tokens acima; o modelo não cria uma paleta independente. +- Gradientes ficam restritos ao fundo atmosférico e não são aplicados dentro da screenshot para não mascarar a interface real. + +## 3. Typography Rules + +O palco mantém as fontes locais e os tokens tipográficos da home; não haverá importação adicional de fonte. + +| Papel | Fonte | Tamanho | Peso | Altura | Tracking | +|---|---|---:|---:|---:|---:| +| Título da narrativa | `var(--font-display)` | `clamp(2.4rem, 6vw, 5.2rem)` | 800 | 0.98 | -0.03em | +| Título do projeto ativo | `var(--font-display)` | `clamp(1.25rem, 2.4vw, 2rem)` | 800 | 1.0 | -0.025em | +| Corpo | `var(--font-mono)` | `clamp(0.82rem, 1.2vw, 1rem)` | 400 | 1.7 | normal | +| Tab/browser chrome | `var(--font-mono)` | 10–11px | 600 | 1.2 | 0.06–0.12em | +| CTA | `var(--font-mono)` | 11px | 700 | 1.0 | 0.10em | +| Metadados | `var(--font-mono)` | 9–10px | 600 | 1.2 | 0.12–0.16em | + +### Regras tipográficas + +- Títulos permanecem em caixa alta apenas quando já seguem a convenção da home. +- Texto de tab usa nomes reais dos projetos; não abrevia nomes de forma ambígua. +- O screenshot pode conter a tipografia própria do projeto. Não aplicar filtro ou blend que prejudique a leitura. +- Para este estilo editorial, títulos não usam gradiente nem `text-shadow`; a profundidade vem do modelo, da luz e do contraste. +- Não usar fontes externas, emoji ou ícones improvisados. + +## 4. Component Stylings + +### NotebookStory shell + +**Default:** fundo `var(--notebook-bg)`, `min-height: 100dvh`, overflow horizontal bloqueado, conteúdo centrado em `max-width: 1240px`. + +**Active:** o palco recebe `data-phase` e `data-project-id`; apenas o projeto corrente usa o acento visual. + +**Focus:** o foco nunca fica no Canvas; controles DOM recebem `outline: 2px solid var(--notebook-accent)` e `outline-offset: 3px`. + +**Disabled:** controles de projeto indisponíveis não aparecem na tablist; o estado não será apenas visualmente desabilitado. + +### Browser chrome + +- Janela com raio discreto (`var(--r-md)`), borda de 1px e fundo quase preto. +- Barra superior separa tabs, endereço e controles decorativos. +- A barra permanece estável; trocar de projeto muda o conteúdo e o estado ativo, não move o frame inteiro. +- Ícones de janela são decorativos e `aria-hidden`; não usar símbolos de texto como controles. + +### Project tabs + +As tabs são `button`s reais dentro de `role="tablist"`. + +```css +.notebook-tab { + min-height: 44px; + border: 1px solid transparent; + background: transparent; + color: var(--notebook-text-tertiary); + transition: + color 180ms var(--ease-out), + background-color 180ms var(--ease-out), + border-color 180ms var(--ease-out), + transform 180ms var(--ease-out); +} + +.notebook-tab:hover { + border-color: var(--notebook-border); + background: var(--notebook-surface-hover); + color: var(--notebook-text); +} + +.notebook-tab[aria-selected="true"] { + border-color: var(--notebook-accent); + background: rgb(var(--notebook-accent-rgb) / 0.10); + color: var(--notebook-accent); +} + +.notebook-tab:active { + transform: scale(0.98); +} + +.notebook-tab:focus-visible { + outline: 2px solid var(--notebook-accent); + outline-offset: 3px; +} + +.notebook-tab:disabled { + cursor: not-allowed; + opacity: 0.42; +} +``` + +### Cursor visual + +- Elemento decorativo com `aria-hidden="true"` e `pointer-events: none`. +- Desenhado em CSS/SVG simples, sem seguir o cursor global do site. +- Estado normal: seta clara pequena. +- Estado de hover: halo de `var(--notebook-accent)` com escala limitada. +- Estado de click: anel de 1 pulso, sem `filter: blur()` em movimento. +- O cursor nunca será a única indicação da tab ativa. + +### CTA `Abrir projeto` + +- Default: bloco preenchido com `var(--notebook-accent)` e texto `var(--ink)`. +- Hover: fundo `var(--notebook-accent-hover)`, texto `var(--ink)`. +- Active: escala `0.98`. +- Focus: anel de acento visível. +- Disabled: não renderizar se o projeto não possuir `demoUrl` aprovado. +- A ação abre o deployment em nova aba com `target="_blank"` e `rel="noopener noreferrer"`. + +## 5. Layout Principles + +### Container + +- Max width: `1240px`, igual às demais seções. +- Padding horizontal: `24px` em mobile/tablet e `64px` em desktop amplo. +- Texto narrativo: máximo de `36ch` para preservar hierarquia. +- Palco 3D: área mínima de `min(58vw, 760px)` em desktop. + +### Desktop + +```css +.notebook-story-grid { + display: grid; + grid-template-columns: minmax(260px, 0.72fr) minmax(520px, 1.28fr); + align-items: center; + gap: clamp(24px, 5vw, 80px); +} +``` + +- O notebook ocupa a coluna visual maior. +- O browser chrome não pode tocar as bordas do viewport. +- O texto não deve ser colocado sobre a área mais clara da tela. +- A indicação de progresso fica próxima ao rodapé, seguindo o HUD atual do `ScrollStory`. + +### Scroll runway + +- Runway inicial: `520vh`, recalculável conforme o número de tabs. +- Palco: `position: sticky; top: 0; height: 100dvh`. +- O sticky libera no final da fase de fechamento; não usar `position: fixed` global. +- O próximo elemento no DOM é `About`; não inserir uma faixa horizontal intermediária. + +### Espaçamento + +- Entre kicker e título: `var(--sp-3)`. +- Entre texto e metadata do projeto: `var(--sp-8)`. +- Entre tabs: `4px` a `8px` conforme largura. +- CTA e tabs devem ter área de toque mínima de `44px`. + +## 6. Depth & Elevation + +| Nível | Tratamento | Uso | +|---|---|---| +| Flat | `var(--ink)` + borda sutil | Fundo e áreas sem foco | +| Surface | plano quase preto + borda `var(--notebook-border)` | Browser chrome e tablist | +| Elevated | sombra curta, opacidade baixa | Browser sobre a tela do notebook | +| Device | luz direcional + metalness moderado | Corpo e tampa do GLB | +| Focus | linha/halo de `var(--notebook-accent)` | Tab ativa, progresso e cursor | + +Regras: + +- Não usar sombra gigantesca para esconder recortes do modelo. +- Não animar `box-shadow` em todos os frames. +- Reflexos do metal devem ser discretos e vir da iluminação R3F. +- O conteúdo da tela deve continuar mais legível que os efeitos de profundidade. + +## 7. Animation & Interaction + +### Filosofia de movimento + +O objeto deve se comportar como um objeto com massa: aproxima, articula na dobradiça, estabiliza, recebe input e fecha. Cada movimento tem função narrativa; não haverá rotação contínua gratuita. + +### Contrato de progresso + +Um único valor `progress ∈ [0, 1]` dirige: + +- transformação do notebook; +- intensidade da tela; +- posição do cursor visual; +- projeto ativo; +- barra de progresso; +- entrada e saída do texto contextual. + +O scroll para cima deve recalcular exatamente os mesmos valores em sentido inverso. Não usar timers para alterar o estado narrativo. + +### Timeline + +| Faixa | Estado | Animação principal | Conteúdo | +|---:|---|---|---| +| `0.00–0.10` | `approach` | notebook entra, escala estabiliza, luz de bancada acende | kicker geral | +| `0.10–0.25` | `open` | tampa gira de fechada para aproximadamente 105° | título da narrativa | +| `0.25–0.34` | `boot` | tela ganha brilho; browser chrome revela | texto curto de contexto | +| `0.34–0.44` | `cursor-in` | cursor entra pela lateral e aponta para a primeira tab | tab 01 preparada | +| `0.44–0.78` | `browse` | quatro ciclos de mover, clicar, trocar screenshot e manter leitura | uma tab ativa por vez | +| `0.78–0.87` | `hold` | última tela permanece; metadata e CTA ganham foco | projeto corrente | +| `0.87–1.00` | `close` | cursor sai, brilho reduz, tampa fecha, palco perde opacidade | próximo conteúdo | + +Cada ciclo de tab terá uma janela de leitura maior que a janela de clique. A troca não deve parecer um carrossel: a moldura permanece fixa e somente o conteúdo da viewport muda com crossfade curto. + +### Rig 3D + +- `base` permanece estável e recebe leve deslocamento de entrada. +- `lid` é o único elemento que rotaciona na abertura/fechamento. +- O plano de tela acompanha a transformação local da tampa. +- A câmera mantém perspectiva 3/4 frontal; mudanças de FOV são proibidas durante a troca de tabs. +- A rotação livre do celular existente será removida; não reaproveitar `motionRig` como turntable. +- O efeito de aproximação da câmera será limitado a uma pequena variação de posição, sem zoom agressivo. + +### Tela e cursor + +A tela será um componente DOM 3D com `Html transform` do Drei, alinhado ao plano interno do modelo. Isso permite que o browser chrome e as tabs sejam reais, acessíveis e clicáveis. + +- Screenshot: `` local com `object-fit: cover` e recorte definido por projeto. +- Browser chrome: DOM, não desenhado em Canvas. +- Cursor: elemento DOM dentro do mesmo plano da tela, movido por timeline. +- Clique coreografado: altera `aria-selected`, `activeProject` e screenshot somente na janela de troca. +- O componente deve pré-carregar a imagem atual e a próxima; as demais entram sob demanda. + +### Interação do visitante + +- O visitante pode rolar e rebobinar a sequência normalmente. +- Foco de teclado não altera o scroll automaticamente. +- `ArrowLeft`/`ArrowRight` mudam a tab selecionada; `Home`/`End` saltam para a primeira/última. +- Clique numa tab usa scroll suave nativo até a janela de progresso daquela tab; depois a timeline continua sendo a fonte de verdade. +- Clique em `Abrir projeto` é a única ação que sai do portfólio e abre um deployment. +- A coreografia pode ser interrompida pelo input sem deixar dois cursores ou duas tabs ativas. +- O cursor visual não acompanha `pointermove` global; o cursor global existente continua com seu contrato atual. + +### ScrollTrigger + +Usar o padrão existente: + +```ts +const timeline = gsap.timeline({ + defaults: { ease: "none" }, + scrollTrigger: { + trigger: section, + start: "top top", + end: "bottom bottom", + scrub: 0.85, + invalidateOnRefresh: true, + }, +}); +``` + +- Não criar um segundo listener global de scroll. +- Atualizações de texto e `aria-selected` devem ser quantizadas por mudança de tab, não por frame. +- A animação deve ser destruída via `gsap.context().revert()` no unmount. +- O Canvas deve pausar quando o palco estiver fora da viewport. + +### Reduced motion e fallback + +```css +@media (prefers-reduced-motion: reduce) { + .notebook-story-stage { + min-height: auto; + } + + .notebook-story-sticky { + position: static; + height: auto; + } + + .notebook-cursor { + display: none; + } +} +``` + +Com `prefers-reduced-motion: reduce`: + +- não inicializar GSAP, ScrollTrigger ou WebGL; +- renderizar notebook aberto como mockup estático ou imagem do modelo; +- mostrar uma tab selecionada e a tablist completa; +- manter título, descrição, screenshot e CTA em fluxo normal; +- permitir troca manual por teclado e clique. + +## 8. Do's and Don'ts + +### Do + +- Usar screenshots reais já versionados em `public/assets/prints`. +- Fazer o browser chrome permanecer fixo enquanto as tabs trocam. +- Dar tempo suficiente para o visitante entender cada projeto. +- Manter o notebook como protagonista visual e o texto como orientação. +- Usar uma única variável de progresso para toda a cena. +- Parar renderização quando o palco estiver invisível. +- Manter os controles DOM acessíveis mesmo quando o modelo 3D falhar. +- Atualizar `CREDITS.md` com o modelo e sua licença. + +### Don't + +- Não usar iframe ou rede externa para montar a tela durante o scroll. +- Não transformar screenshots em uma faixa horizontal novamente. +- Não criar um segundo Canvas WebGL para o notebook. +- Não rotacionar o notebook inteiro como se fosse um celular. +- Não animar `filter: blur()` em elementos que se movem a cada frame. +- Não esconder tabs clicáveis dentro de um Canvas sem equivalente DOM. +- Não usar o cursor global como substituto do estado ativo da tab. +- Não deixar o usuário preso no sticky depois que a tampa fechou. +- Não mostrar projetos fora do snapshot aprovado de saúde. +- Não iniciar timers independentes que quebrem o rewind do scroll. + +## 9. Responsive Behavior + +| Breakpoint | Layout | Comportamento | +|---|---|---| +| Desktop `>= 1025px`, hover disponível | duas colunas | GLB CC0, `Html transform`, sticky + scrub completo | +| Tablet `768–1024px` | uma coluna compacta | 3D somente com hover disponível e espaço suficiente; caso contrário, fallback 2D | +| Mobile `< 768px` ou pointer coarse | fluxo normal | sem WebGL; mockup 2D aberto, tablist em 2 colunas ou wrap, screenshot única | +| Qualquer largura + reduced motion | fluxo normal | fallback estático acessível, sem pin, cursor automático ou scrub | + +### Mobile + +- Nunca exigir que o visitante acompanhe uma tampa 3D em uma tela pequena. +- O mockup 2D deve manter proporção de notebook, mas priorizar screenshot, tabs e CTA. +- Tabs podem quebrar em duas linhas; não usar `white-space: nowrap` em uma faixa que cause overflow. +- O screenshot ativo ocupa a largura disponível com `aspect-ratio` estável. +- O CTA fica depois do screenshot, com largura confortável para toque. +- A seção não deve criar overflow horizontal em viewport de `320px`. + +## Arquitetura e integração com o repositório + +### Composição da home + +`CinematicHome.tsx` deve passar `projects` para o novo palco e ficar nesta sequência: + +```text + + + + + + +... +``` + +O `` será removido. O conteúdo de contexto atualmente presente no `ScrollStory` será adaptado para o notebook; não haverá duas introduções concorrentes. + +### Componentes propostos + +- `app/components/story/NotebookStory.tsx`: orquestra runway, ScrollTrigger, capítulos, estado de tab e fallback. +- `app/components/story/NotebookStage3D.tsx`: carrega o GLB, monta o rig, câmera, luzes e boundary de falha. +- `app/components/story/NotebookBrowser.tsx`: browser chrome, tabs, screenshot, cursor visual e CTA. +- `app/components/story/notebookProjects.ts`: mapa entre IDs de projeto e assets de screenshot, sem chamada de rede. +- `app/components/cinematic/CinematicHome.tsx`: integração e remoção do `PrintReel`. +- `public/models/laptop-kenney.glb`: asset CC0 aprovado após validação de nós e escala. +- `CREDITS.md`: fonte, autor, licença, URL e modificações do modelo. + +Depois da migração, remover os caminhos sem consumidores: + +- `PrintReel.tsx`; +- `StoryStage3D.tsx`; +- `phoneScreenTexture.ts`; +- testes específicos desses componentes, substituídos por testes do notebook. + +`ReelDirector.tsx` continua responsável pelas animações declarativas das seções restantes. + +## Dados, assets e licenciamento + +### Projetos + +A fonte continua sendo `PortfolioProject[]` carregada no servidor. O mapa visual deve usar: + +- `id` para escolher o screenshot local; +- `title` e `subtitle` para metadata; +- `technologies` para a linha técnica; +- `demoUrl` para o CTA; +- a lista já filtrada pelo snapshot de saúde para decidir quais tabs existem. + +A ordem inicial é a já usada no filmstrip atual: + +1. `nutriscan` — `/assets/prints/print-nutriscan.png`; +2. `sc-plus` — `/assets/prints/print-scplus.png`; +3. `sev-modaintima` — `/assets/prints/print-sev.png`; +4. `as-servicos` — `/assets/prints/print-asservicos.png`. + +Se um projeto live não tiver screenshot mapeado, ele não entra na narrativa até o asset ser incluído; não renderizar imagem quebrada ou placeholder colorido. + +### Modelo 3D + +Candidato aprovado para investigação: [Laptop — Kenney / Poly Pizza](https://poly.pizza/m/GnbwSUiVty), listado como OBJ/GLTF e Public Domain (CC0). A licença pública também está referenciada em [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/). + +Antes de integrar: + +- baixar uma cópia local, não carregar o GLB por URL remota; +- inspecionar o Outliner e confirmar objetos separados para base, tampa, tela e teclado; +- confirmar origem da tampa próxima à dobradiça; +- corrigir escala, normais, materiais e pivô offline; +- registrar fonte e modificações em `CREDITS.md`; +- se o arquivo não permitir separar a tampa, escolher outro asset CC0 ou aplicar o fallback procedural; não animar o modelo inteiro como substituto. + +### Tela + +A tela usa os PNGs existentes, sem captura em runtime. Não haverá requisição externa durante renderização, nem dependência de disponibilidade dos deployments para a animação aparecer. + +## Acessibilidade + +- A seção recebe `aria-label="Projetos em produção no notebook"`. +- A tablist usa `role="tablist"`; cada tab usa `role="tab"`, `aria-selected` e `aria-controls`. +- O painel ativo usa `role="tabpanel"`, `aria-labelledby` e nome do projeto. +- Mudança de projeto atualiza uma região `aria-live="polite"` curta, sem narrar cada frame. +- O Canvas e o cursor coreografado são decorativos para tecnologia assistiva. +- O CTA real permanece fora da textura e é alcançável por teclado. +- A ordem de tabulação segue texto → tabs → CTA, sem focar controles decorativos. +- O fallback sem JavaScript continua apresentando título, tabs, screenshot e links no fluxo normal. +- Foco visível, contraste e áreas de toque seguem os tokens globais já existentes. + +## Performance e limites + +- Um único Canvas WebGL para o bloco `NotebookStory`; não criar um segundo contexto para browser chrome, tabs ou fallback. +- Renderers 3D existentes da hero devem ser pausados quando estiverem fora da viewport, evitando duas cenas animando ao mesmo tempo. +- WebGL somente em desktop com hover disponível. +- Canvas pausado fora da viewport por `IntersectionObserver` ou equivalente do palco. +- `dpr` limitado a `[1, 1.5]` no notebook. +- GLB local otimizado para no máximo 2 MB transferidos; texturas externas não são permitidas. +- Uma imagem atual e uma próxima pré-carregadas; demais lazy. +- Screenshot não deve ser redesenhada por frame; apenas a troca de `src` e o movimento de cursor acompanham a timeline. +- Nenhum `fetch` de deployment dentro de `NotebookStory`. +- Nenhuma nova biblioteca de scroll ou de cursor. +- Alvo de verificação: pelo menos 50 FPS em desktop intermediário durante scrub contínuo; se o modelo não atingir isso, reduzir luzes/geometria antes de adicionar efeitos. +- Mobile e reduced motion não criam Canvas WebGL. + +## Critérios de aceite + +### Comportamento + +1. A seção inicia com notebook fechado. +2. A tampa abre pela dobradiça, sem girar o corpo inteiro. +3. O browser chrome aparece antes da primeira troca de tab. +4. O cursor visual move-se até a primeira tab, clica e troca o conteúdo. +5. As quatro tabs aprovadas aparecem na ordem definida quando os quatro projetos estão live. +6. Cada tab mostra screenshot, nome, metadata e CTA correspondente. +7. A tela permanece no mesmo enquadramento; não existe deslocamento horizontal de cards. +8. O notebook fecha depois da última leitura. +9. Ao terminar o fechamento, `About` aparece no fluxo normal sem salto ou seção presa. +10. Ao rolar para cima, todas as fases rebobinam sem duplicar tabs, cursores ou listeners. + +### Acessibilidade e fallback + +11. Tablist, tabs, painel e CTA são encontrados no accessibility tree. +12. Teclado consegue trocar tabs sem depender do cursor animado. +13. `prefers-reduced-motion` desativa pin, scrub, cursor automático e WebGL. +14. Pointer coarse/mobile não recebe Canvas WebGL nem overflow horizontal. +15. Falha do GLB mantém screenshot, tabs e CTA no mockup fallback. +16. Um projeto sem health check aprovado não aparece na narrativa. + +### Qualidade técnica + +17. Não há iframe ou request remoto no componente. +18. O bloco `NotebookStory` mantém somente um Canvas WebGL; controls e fallback não criam canvases adicionais. +19. O unmount remove ScrollTrigger, GSAP context e observers. +20. `PrintReel`, o rig do celular e a textura antiga são removidos depois de não haver referências. +21. `CREDITS.md` contém atribuição e licença do notebook. +22. O build de produção continua passando. + +## Plano de testes e verificação + +### Testes unitários/componentes + +- mapa de projeto: filtra IDs live e ignora screenshot ausente; +- função de timeline: cada faixa de `progress` produz a fase esperada; +- seleção de tab: `aria-selected`, `aria-controls` e painel acompanham a mudança; +- teclado: setas, `Home`, `End`, `Enter` e `Space`; +- CTA: URL, `target` e `rel` corretos; +- reduced motion: fallback sem inicializar GSAP/WebGL; +- falha do GLB: boundary renderiza mockup sem perder controles. + +### Smoke browser + +Em Chromium real, verificar: + +1. carregar a home sem erro de console; +2. confirmar notebook fechado no início da seção; +3. avançar o scroll até abertura, boot e cada tab; +4. voltar o scroll e confirmar rewind visual; +5. clicar numa tab e confirmar seek para sua janela; +6. usar teclado no tablist; +7. chegar ao fim e confirmar que `About` segue normalmente; +8. medir ausência de overflow horizontal em desktop e `320px`; +9. repetir com reduced motion e confirmar fluxo estático; +10. confirmar que o CTA abre somente quando acionado pelo visitante. + +### Verificações de entrega + +```bash +pnpm exec jest --runInBand +pnpm exec tsc --noEmit +pnpm exec biome check app/components/story app/components/cinematic/CinematicHome.tsx +pnpm run build +``` + +## Ordem de implementação + +1. Validar, otimizar e creditar o GLB CC0. +2. Criar o rig `base`/`lid` e o fallback procedural no palco existente. +3. Criar `NotebookBrowser` com DOM 3D, tabs, screenshot e CTA. +4. Migrar a timeline do `ScrollStory` para o contrato de fases do notebook. +5. Passar `projects` de `CinematicHome` e filtrar a lista visual pelos dados aprovados. +6. Remover `PrintReel`, celular e textura antiga após atualizar todos os consumidores. +7. Implementar reduced motion, mobile e falhas do modelo. +8. Executar testes, smoke browser, build e revisão visual desktop/mobile. + +A implementação só começa depois da revisão deste spec; qualquer mudança no número de Canvas, origem dos screenshots, ordem de projetos ou comportamento de fallback deve ser registrada neste documento antes do código.