feedback-collector 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +156 -0
- package/package.json +44 -0
- package/skills/feedback-collector-setup/SKILL.md +163 -0
- package/src/feedback-collector.js +461 -0
- package/src/index.d.ts +6 -0
- package/src/index.js +14 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bruno Santos / Bananas Global
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Feedback Collector
|
|
2
|
+
|
|
3
|
+
Coletor de feedback visual para desenvolvimento assistido por IA. Você navega pelo seu app em dev, segura ALT, clica nos elementos que quer mudar, escreve uma instrução curta para cada um e, no final, exporta um backlog em markdown — com source mapping (`arquivo:linha`) — pronto para colar de uma vez no Claude Code ou em outro agente de código.
|
|
4
|
+
|
|
5
|
+
## O problema
|
|
6
|
+
|
|
7
|
+
O fluxo atual de iteração visual com agentes de código é serial: você aponta um elemento, descreve a mudança, espera o agente executar, e só então aponta o próximo. Para quem trabalha com design e front-end, isso é lento por dois motivos: descrever coisas visuais em texto é impreciso, e o ciclo clica-espera-clica desperdiça o tempo em que você já sabe as próximas cinco mudanças que quer fazer.
|
|
8
|
+
|
|
9
|
+
A ideia central deste projeto é **desacoplar a coleta da execução**. Você acumula feedbacks anotados enquanto navega (como faria numa ferramenta de revisão tipo Pastel ou BugHerd), e despeja tudo de uma vez para a IA — mas com um payload técnico que ferramentas de anotação para humanos não capturam: elemento DOM, seletor estável, computed styles e, principalmente, o mapeamento para o arquivo e linha no código-fonte.
|
|
10
|
+
|
|
11
|
+
## Posicionamento (o que já existe e qual é o gap)
|
|
12
|
+
|
|
13
|
+
| Ferramenta | O que faz | Por que não resolve |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| **stagewise** | Toolbar que conecta seleção de elementos a agentes no editor | Fluxo orientado a execução imediata, não a backlog em lote; empresa pivotando para browser/IDE próprios |
|
|
16
|
+
| **Pastel / BugHerd** | Anotação visual em site vivo, comentários viram tarefas | Output para humanos ("o botão tá grande"); sem acesso ao código, source mapping é arquitetonicamente impossível |
|
|
17
|
+
| **Frontman** | Mapeamento DOM → arquivo/linha, BYOK | Sem modelo de fila + flush em lote |
|
|
18
|
+
| **Chrome DevTools MCP** | Dá contexto de browser ao agente | Bare-bones, sem coleta acumulada nem anotação |
|
|
19
|
+
|
|
20
|
+
O gap: **fila persistente de feedbacks anotados + payload técnico com source mapping + export em lote otimizado para agentes**. É isso que este projeto entrega.
|
|
21
|
+
|
|
22
|
+
## Estado atual: POC empacotada
|
|
23
|
+
|
|
24
|
+
A POC é um único arquivo (`src/feedback-collector.js`) em JavaScript vanilla, sem dependências, sem backend, sem build — agora publicado como package npm (`feedback-collector`) e instalado em um primeiro projeto de produção real (ver `docs/decisoes.md`). Ela existe para validar a hipótese mais arriscada do projeto:
|
|
25
|
+
|
|
26
|
+
> Um backlog source-mapeado despejado de uma vez faz o Claude Code acertar os arquivos e produzir edits coerentes, de forma mais rápida que o fluxo serial atual.
|
|
27
|
+
|
|
28
|
+
Deliberadamente fora do escopo da POC: extensão de navegador, screenshots, auth, persistência em servidor, MCP, multi-usuário. Nada disso valida a hipótese; tudo isso é v1 em diante.
|
|
29
|
+
|
|
30
|
+
### Setup
|
|
31
|
+
|
|
32
|
+
1. Instale o package:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm i -D feedback-collector
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
2. Carregue via import dinâmico (o script é um side effect — injeta o picker ao ser importado):
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
// Em um componente raiz (App.tsx, _app.tsx, layout.tsx...)
|
|
42
|
+
if (process.env.NODE_ENV === 'development') {
|
|
43
|
+
import('feedback-collector');
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Em app com auth dá para ir além de dev: renderizar o loader server-side condicionado ao papel do usuário (ex. só owner/admin) e usar o picker **em produção**, contra dados reais — o script só lê o DOM que o próprio usuário já vê. Sites sem build: use o IIFE cru em `feedback-collector/script` via `<script src>`.
|
|
48
|
+
|
|
49
|
+
3. Configure o source mapping (é ele que injeta `arquivo:linha` no DOM):
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npm i -D @react-dev-inspector/babel-plugin
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
e plugue no bundler — receitas testadas por stack (Next webpack, Vite, prod vs dev, armadilhas do Babel 8) em [`skills/feedback-collector-setup/SKILL.md`](skills/feedback-collector-setup/SKILL.md), que também serve de skill para agentes de código instalarem/removerem isso sozinhos.
|
|
56
|
+
|
|
57
|
+
> ⚠️ **React 19:** o fallback via fiber (`_debugSource`) foi removido do React 19 — sem o plugin de build **não há** arquivo:linha (o resto do payload continua funcionando).
|
|
58
|
+
|
|
59
|
+
### Uso
|
|
60
|
+
|
|
61
|
+
Segure **ALT** para entrar em modo picker — os elementos ganham highlight com o nome do arquivo no tooltip. **ALT + clique** captura o elemento e abre um campo para sua instrução ("diminuir o padding", "esse botão deveria ser secundário"). O painel no canto inferior direito acumula os itens; eles persistem em `localStorage`, então sobrevivem a reload e navegação. Quando terminar a sessão, **"Copiar backlog"** gera o markdown numerado no clipboard — cole no Claude Code e peça para executar.
|
|
62
|
+
|
|
63
|
+
O contador **"N com source"** no header do painel é o termômetro da POC: se a maioria dos itens está sendo capturada sem source mapping, o problema está no setup do plugin, e vale resolver antes de testar o flush.
|
|
64
|
+
|
|
65
|
+
## Como o script funciona por dentro
|
|
66
|
+
|
|
67
|
+
O script é auto-executável (IIFE) e se protege contra dupla injeção via flag em `window`. Ele opera em quatro camadas:
|
|
68
|
+
|
|
69
|
+
### 1. Picker (interação)
|
|
70
|
+
|
|
71
|
+
Listeners globais de teclado e mouse em fase de captura (`capture: true`, para interceptar antes do app). Segurar ALT ativa o modo picker; um `div` overlay com `pointer-events: none` segue o cursor usando `document.elementFromPoint` + `getBoundingClientRect`, desenhando o highlight. No clique, `preventDefault` + `stopPropagation` impedem que a ação real do elemento dispare (um botão não é clicado de verdade), e o elemento é capturado.
|
|
72
|
+
|
|
73
|
+
### 2. Extração do payload (a inteligência)
|
|
74
|
+
|
|
75
|
+
Para cada elemento capturado, o script monta um objeto com:
|
|
76
|
+
|
|
77
|
+
- **Source mapping**, em ordem de tentativa: (a) atributos `data-inspector-relative-path/-line/-column` injetados pelo react-dev-inspector no elemento ou em ancestrais; (b) fallback lendo o fiber interno do React (`_debugSource`, disponível em dev builds do React < 19), subindo a árvore de owners; (c) se nada disso existir, o item fica sem source, mas ainda carrega o resto do payload.
|
|
78
|
+
- **Cadeia de componentes**: nomes dos ~3 componentes React mais próximos, extraídos do fiber, úteis quando não há arquivo:linha.
|
|
79
|
+
- **Seletor CSS estável**: prioriza `id`, depois atributos estáveis (`data-testid`, `aria-label`, `name`), depois classes — filtrando classes que parecem geradas por hash/CSS Modules — e por fim `nth-of-type`. Para de subir na árvore assim que o seletor parcial já é único no documento.
|
|
80
|
+
- **Computed styles curados**: um subconjunto de ~20 propriedades relevantes a layout e aparência, descartando valores default barulhentos (`none`, `auto`, `0px`...). Isso mantém o payload pequeno sem perder o que importa para ajustes visuais.
|
|
81
|
+
- **Contexto**: rota atual, tag, texto visível (truncado), `outerHTML` truncado em 600 chars, bounding box e viewport.
|
|
82
|
+
|
|
83
|
+
### 3. Fila (estado)
|
|
84
|
+
|
|
85
|
+
Array em memória espelhado em `localStorage` a cada mutação. Observação: `localStorage` é por origem, então `localhost:3000` e `localhost:5173` têm filas independentes. Não há servidor nem escrita em disco — tudo vive no navegador.
|
|
86
|
+
|
|
87
|
+
### 4. Painel e export (saída)
|
|
88
|
+
|
|
89
|
+
O painel é DOM puro injetado pelo próprio script (re-render por innerHTML a cada mudança — suficiente para a POC). O export concatena os itens em markdown numerado: título com a instrução, rota, `arquivo:linha`, seletor, styles atuais e o HTML do elemento em bloco de código. O cabeçalho do markdown já instrui o agente a agrupar edits do mesmo arquivo — mitigação barata para o risco de dispersão em backlogs grandes.
|
|
90
|
+
|
|
91
|
+
### Decisão de design: sem screenshot (por enquanto)
|
|
92
|
+
|
|
93
|
+
Cada screenshot custa ~1 a 1,8k tokens; um backlog de 15 itens com prints estoura contexto rápido. A aposta da POC é que, para ajustes de layout e estilo, source mapping + DOM + styles + instrução textual bastam. Captura visual só entra se o teste mostrar que não basta — e aí no formato econômico: um screenshot de página com pins numerados + crops apertados por elemento.
|
|
94
|
+
|
|
95
|
+
## Compatibilidade
|
|
96
|
+
|
|
97
|
+
| Camada | React (Next, Vite, CRA) | Vue / Svelte | Site sem build (HTML, CMS) |
|
|
98
|
+
|---|---|---|---|
|
|
99
|
+
| Picker, painel, fila, export | ✅ | ✅ | ✅ |
|
|
100
|
+
| Seletor, styles, HTML | ✅ | ✅ | ✅ |
|
|
101
|
+
| Source mapping (arquivo:linha) | ✅ via babel-plugin (obrigatório em React 19 — fiber fallback morreu) | 🔧 pequeno ajuste (vue-inspector / Svelte inspector injetam atributos análogos) | ❌ (menos grave: o agente acha via grep no HTML) |
|
|
102
|
+
|
|
103
|
+
A captura em si é framework-agnostic; só o source mapping é acoplado. Suportar Vue/Svelte é acrescentar os atributos deles na função `getSourceInfo` (~10 linhas).
|
|
104
|
+
|
|
105
|
+
## Critérios de sucesso da POC
|
|
106
|
+
|
|
107
|
+
Medir em uma sessão real de trabalho, cronometrada contra o fluxo atual:
|
|
108
|
+
|
|
109
|
+
1. **Precisão**: o agente vai direto no arquivo certo, sem caçar?
|
|
110
|
+
2. **Coerência em lote**: um backlog de ~10 itens produz edits coerentes, ou o agente dispersa e mistura contexto? Se degradar, as respostas prováveis são agrupar o export por arquivo/proximidade ou capar em N itens por flush.
|
|
111
|
+
3. **Velocidade**: foi mais rápido que o clica-espera-clica atual?
|
|
112
|
+
|
|
113
|
+
Se os três derem verde, há produto. Se o item 2 falhar, o valor está no source mapping por clique (não no lote), o que muda o desenho da v1.
|
|
114
|
+
|
|
115
|
+
## Próximos passos
|
|
116
|
+
|
|
117
|
+
**Curto prazo (validação)**
|
|
118
|
+
- [ ] Rodar a POC em 1–2 projetos reais e medir os três critérios acima
|
|
119
|
+
- [ ] Testar o limite do lote: em que N de itens o agente começa a dispersar?
|
|
120
|
+
- [ ] Adicionar suporte a atributos do vue-inspector / Svelte inspector no `getSourceInfo`
|
|
121
|
+
|
|
122
|
+
**v1 (se a POC validar)**
|
|
123
|
+
- [x] Empacotar como dev-dependency npm (`npm i -D feedback-collector`), modelo ESLint/Storybook: instalação por projeto
|
|
124
|
+
- [x] Skill para agentes de código instalarem/removerem sozinhos (`skills/feedback-collector-setup/`)
|
|
125
|
+
- [ ] Embutir o plugin de source mapping no pacote (Vite/Babel/SWC), eliminando o react-dev-inspector como passo separado
|
|
126
|
+
- [ ] Export agrupado por arquivo/proximidade, se o teste de lote indicar necessidade
|
|
127
|
+
- [ ] Nível 1 de integração: endpoint dev local que grava `.feedback/backlog.md` no repo — o agente lê o arquivo em vez de receber colagem
|
|
128
|
+
- [ ] Melhorias de UX no painel: reordenar itens, marcar como resolvido, pausar coleta por iteração (padrão Pastel)
|
|
129
|
+
|
|
130
|
+
**v2 (exploração)**
|
|
131
|
+
- [ ] Captura visual econômica: screenshot de página com pins numerados + crops por elemento, opt-in
|
|
132
|
+
- [ ] Integração via MCP para flush direto no agente, sem clipboard
|
|
133
|
+
- [ ] Sessões nomeadas / múltiplos backlogs por projeto
|
|
134
|
+
|
|
135
|
+
**Explicitamente fora de escopo por ora**: extensão de navegador (perderia o acesso ao build e, com ele, o source mapping — que é o diferencial), auth, backend, multi-usuário.
|
|
136
|
+
|
|
137
|
+
## Estrutura do repo
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
feedback-collector/
|
|
141
|
+
├── README.md # este documento
|
|
142
|
+
├── package.json # publicado no npm como `feedback-collector`
|
|
143
|
+
├── src/
|
|
144
|
+
│ ├── feedback-collector.js # o script (IIFE, side effect)
|
|
145
|
+
│ ├── index.js # entry ESM (import 'feedback-collector')
|
|
146
|
+
│ └── index.d.ts # tipos (package sem API — só side effect)
|
|
147
|
+
├── skills/
|
|
148
|
+
│ └── feedback-collector-setup/
|
|
149
|
+
│ └── SKILL.md # skill p/ agentes: instalar/remover por stack
|
|
150
|
+
└── docs/
|
|
151
|
+
└── decisoes.md # registro de decisões conforme a POC evolui
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Licença
|
|
155
|
+
|
|
156
|
+
MIT (observar que stagewise é AGPL, o que restringe reuso direto de código deles).
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "feedback-collector",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Coletor de feedback visual para desenvolvimento assistido por IA: ALT+clique nos elementos, anote instruções e exporte um backlog em markdown com source mapping (arquivo:linha) pronto para colar no Claude Code ou outro agente.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"types": "./src/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./src/index.d.ts",
|
|
11
|
+
"default": "./src/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./script": "./src/feedback-collector.js"
|
|
14
|
+
},
|
|
15
|
+
"sideEffects": true,
|
|
16
|
+
"files": [
|
|
17
|
+
"src",
|
|
18
|
+
"skills",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"keywords": [
|
|
23
|
+
"feedback",
|
|
24
|
+
"visual-feedback",
|
|
25
|
+
"dev-tools",
|
|
26
|
+
"ai",
|
|
27
|
+
"claude-code",
|
|
28
|
+
"code-agent",
|
|
29
|
+
"source-mapping",
|
|
30
|
+
"react-dev-inspector",
|
|
31
|
+
"inspector",
|
|
32
|
+
"annotation"
|
|
33
|
+
],
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/bananas-global/feedback-collector.git"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/bananas-global/feedback-collector#readme",
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/bananas-global/feedback-collector/issues"
|
|
41
|
+
},
|
|
42
|
+
"author": "Bruno Santos (https://github.com/brucesantos)",
|
|
43
|
+
"license": "MIT"
|
|
44
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: feedback-collector-setup
|
|
3
|
+
description: >
|
|
4
|
+
Instala, configura ou remove o feedback-collector (picker visual ALT+clique →
|
|
5
|
+
backlog markdown com arquivo:linha) em um projeto front-end. Use quando o
|
|
6
|
+
usuário pedir para "instalar o feedback-collector", "adicionar o picker de
|
|
7
|
+
feedback", "configurar source mapping do feedback", "remover/desativar o
|
|
8
|
+
feedback-collector", ou equivalentes em inglês (install/remove feedback
|
|
9
|
+
collector, visual feedback picker). Cobre Next.js (webpack), Vite e sites sem
|
|
10
|
+
build; inclui o caveat de React 19 e o gate para uso em produção.
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# feedback-collector: instalar e remover
|
|
14
|
+
|
|
15
|
+
O package `feedback-collector` é um script client-side puro (IIFE, sem backend):
|
|
16
|
+
segurando ALT o usuário clica em elementos, anota instruções e exporta um
|
|
17
|
+
backlog markdown com `arquivo:linha` para colar em um agente de código. A
|
|
18
|
+
instalação tem DUAS partes independentes:
|
|
19
|
+
|
|
20
|
+
1. **O script** (picker/painel) — trivial, funciona em qualquer stack.
|
|
21
|
+
2. **O source mapping** (`arquivo:linha`) — exige um plugin de build que injeta
|
|
22
|
+
`data-inspector-{relative-path,line,column}` no JSX. É a parte com decisões.
|
|
23
|
+
|
|
24
|
+
Antes de começar, detecte: framework (Next/Vite/CRA/sem build), bundler do dev
|
|
25
|
+
(Next: webpack ou `--turbopack` no script `dev`?), versão do React, e se o
|
|
26
|
+
projeto tem auth (define o gate de produção).
|
|
27
|
+
|
|
28
|
+
## Parte 1 — o script
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm i -D feedback-collector # ou pnpm add -D
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Carregue via componente client montado condicionalmente (React):
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
// components/feedback-collector-loader.tsx
|
|
38
|
+
"use client"; // (Next App Router)
|
|
39
|
+
import { useEffect } from "react";
|
|
40
|
+
|
|
41
|
+
export function FeedbackCollector() {
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
void import("feedback-collector");
|
|
44
|
+
}, []);
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**Onde renderizar — decisão de gate (pergunte ao usuário se ambíguo):**
|
|
50
|
+
|
|
51
|
+
- **Só em dev (padrão seguro):** no layout raiz,
|
|
52
|
+
`{process.env.NODE_ENV === "development" && <FeedbackCollector />}`.
|
|
53
|
+
- **Em produção, gated por usuário:** se o app tem auth e o usuário quer usar o
|
|
54
|
+
picker contra dados reais de prod, renderize num layout autenticado
|
|
55
|
+
condicionado ao papel do usuário (ex.: `{isOwner && <FeedbackCollector />}`).
|
|
56
|
+
O script só lê o DOM que o próprio usuário já vê (não é vetor de vazamento),
|
|
57
|
+
mas usuários comuns NÃO devem ver o painel. O gate server-side também evita
|
|
58
|
+
que o chunk seja baixado por quem não usa.
|
|
59
|
+
|
|
60
|
+
Sites sem build: `<script src="node_modules/feedback-collector/src/feedback-collector.js">`
|
|
61
|
+
(ou copie o arquivo). O entry `feedback-collector/script` aponta pro IIFE cru.
|
|
62
|
+
|
|
63
|
+
## Parte 2 — source mapping (arquivo:linha)
|
|
64
|
+
|
|
65
|
+
**Caveat central: React 19 removeu `_debugSource` do fiber.** O fallback interno
|
|
66
|
+
do script não funciona em React 19 — sem plugin de build, os itens saem SEM
|
|
67
|
+
arquivo:linha (o resto funciona: seletor, styles, HTML). Em React ≤18 o fallback
|
|
68
|
+
existe, mas o plugin ainda é mais confiável.
|
|
69
|
+
|
|
70
|
+
### Next.js (webpack) — receita validada (Next 15, React 19)
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
npm i -D @react-dev-inspector/babel-plugin babel-loader @babel/core @babel/preset-typescript @babel/plugin-syntax-jsx
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
NÃO crie `.babelrc` (desligaria o SWC do projeto inteiro). Em vez disso, um
|
|
77
|
+
pre-pass só do loader no `next.config.ts`:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
webpack: (config, { dev }) => {
|
|
81
|
+
if (dev) { // ver "Em produção?" abaixo antes de remover este gate
|
|
82
|
+
config.module.rules.unshift({
|
|
83
|
+
test: /\.(jsx|tsx)$/,
|
|
84
|
+
exclude: /node_modules/,
|
|
85
|
+
enforce: "pre",
|
|
86
|
+
use: [{
|
|
87
|
+
loader: "babel-loader",
|
|
88
|
+
options: {
|
|
89
|
+
babelrc: false, configFile: false, sourceMaps: false,
|
|
90
|
+
presets: ["@babel/preset-typescript"],
|
|
91
|
+
// syntax-jsx só PARSEIA (SWC segue fazendo JSX→JS depois)
|
|
92
|
+
plugins: ["@babel/plugin-syntax-jsx", "@react-dev-inspector/babel-plugin"],
|
|
93
|
+
},
|
|
94
|
+
}],
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return config;
|
|
98
|
+
},
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Armadilhas conhecidas (Babel 8): `isTSX`/`allExtensions` foram REMOVIDOS do
|
|
102
|
+
preset-typescript — não os passe; use `@babel/plugin-syntax-jsx` pra habilitar
|
|
103
|
+
JSX. Aplique o loader a server E client (só client causa hydration mismatch).
|
|
104
|
+
|
|
105
|
+
Se o `dev` script usa `--turbopack`: o hook `webpack()` não roda. Opções:
|
|
106
|
+
remover a flag em dev, ou usar `experimental.swcPlugins` (abaixo).
|
|
107
|
+
|
|
108
|
+
### Next.js — em produção?
|
|
109
|
+
|
|
110
|
+
Trade-offs de tirar o `if (dev)` (decisão do usuário, nunca automática):
|
|
111
|
+
- Os paths de `src/` ficam embutidos nos chunks JS **públicos** (servidos sem
|
|
112
|
+
auth) — vazamento de estrutura, severidade baixa mas permanente.
|
|
113
|
+
- O build de produção passa a rodar o pre-pass Babel (mais lento).
|
|
114
|
+
Validado em produção real (Next 15): funciona; documente o trade-off num
|
|
115
|
+
comentário no config.
|
|
116
|
+
|
|
117
|
+
### Vite (React)
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
npm i -D vite-plugin-react-dev-inspector
|
|
121
|
+
```
|
|
122
|
+
Registre o plugin no `vite.config.ts` ANTES do plugin react (ver docs do
|
|
123
|
+
pacote). Alternativa: `@react-dev-inspector/babel-plugin` via option `babel`
|
|
124
|
+
do `@vitejs/plugin-react`.
|
|
125
|
+
|
|
126
|
+
### Alternativa SWC (sem Babel)
|
|
127
|
+
|
|
128
|
+
`experimental.swcPlugins` no Next + `swc-plugin-react-source-string` injeta
|
|
129
|
+
`data-source="arquivo:linha"`. Mantém o SWC (build rápido), MAS: experimental
|
|
130
|
+
desde Next 12.2 (2022, verificado ainda experimental em 2026) e plugins Wasm
|
|
131
|
+
quebram entre versões do `swc_core` do Next. Exige adaptar `getSourceInfo()` no
|
|
132
|
+
script pra ler `data-source` além de `data-inspector-*` (~5 linhas). Só sugira
|
|
133
|
+
se o custo do build Babel for dor explícita.
|
|
134
|
+
|
|
135
|
+
### Vue / Svelte
|
|
136
|
+
|
|
137
|
+
`vue-inspector` / inspector do Svelte injetam atributos análogos; hoje exige
|
|
138
|
+
adaptar `getSourceInfo()` no script (não suportado out-of-the-box).
|
|
139
|
+
|
|
140
|
+
## Verificação (sempre faça)
|
|
141
|
+
|
|
142
|
+
1. `typecheck`/`lint`/`build` do projeto passam.
|
|
143
|
+
2. Suba o dev server, faça request numa rota e confirme no HTML SSR/DOM:
|
|
144
|
+
`grep -o 'data-inspector-relative-path="[^"]*"' | sort -u` deve listar
|
|
145
|
+
paths reais do projeto. Zero ocorrências = plugin não está rodando.
|
|
146
|
+
3. Se ligou em prod: rode o build e confirme `grep -rl data-inspector .next/static`.
|
|
147
|
+
4. No browser: console mostra `[feedback-collector] ativo`; ALT+hover mostra
|
|
148
|
+
tooltip com o arquivo.
|
|
149
|
+
|
|
150
|
+
## Remoção
|
|
151
|
+
|
|
152
|
+
Três níveis — pergunte qual o usuário quer:
|
|
153
|
+
|
|
154
|
+
1. **Desligar o painel:** remova o `<FeedbackCollector />` do layout. O babel
|
|
155
|
+
pre-pass continua (atributos ainda no bundle).
|
|
156
|
+
2. **Limpar o build:** re-gate o pre-pass com `if (dev)` (ou remova o bloco
|
|
157
|
+
`webpack`) — prod volta a SWC puro, sem paths nos chunks.
|
|
158
|
+
3. **Remoção total:** (1) + (2) + deletar o componente loader +
|
|
159
|
+
`npm rm feedback-collector @react-dev-inspector/babel-plugin babel-loader @babel/core @babel/preset-typescript @babel/plugin-syntax-jsx`
|
|
160
|
+
+ rebuild. Nada toca banco/auth — é tudo client-side + config de build.
|
|
161
|
+
|
|
162
|
+
Os itens capturados vivem em `localStorage` (chave por origem); remover o
|
|
163
|
+
script não apaga nada sensível.
|
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* feedback-collector.js — POC de coletor de feedback visual para Claude Code
|
|
3
|
+
* ---------------------------------------------------------------------------
|
|
4
|
+
* USO
|
|
5
|
+
* 1. (Recomendado) Instale o react-dev-inspector para source mapping:
|
|
6
|
+
* npm i -D react-dev-inspector
|
|
7
|
+
* e adicione o plugin ao seu bundler (Babel/SWC/Vite — ver docs do pacote).
|
|
8
|
+
* Ele injeta data-inspector-relative-path / -line / -column no DOM em dev.
|
|
9
|
+
* Sem ele o script ainda funciona, mas cai pro fallback (fiber _debugSource)
|
|
10
|
+
* e, em último caso, só seletor CSS.
|
|
11
|
+
*
|
|
12
|
+
* 2. Carregue este arquivo SOMENTE em dev. Exemplos:
|
|
13
|
+
* - Vite/Next (em um componente raiz ou _app):
|
|
14
|
+
* if (process.env.NODE_ENV === 'development') import('./feedback-collector.js')
|
|
15
|
+
* - Ou <script src="/feedback-collector.js"></script> no index.html de dev.
|
|
16
|
+
*
|
|
17
|
+
* INTERAÇÃO
|
|
18
|
+
* - Segure ALT: entra em modo picker (elementos ganham highlight no hover).
|
|
19
|
+
* - ALT + clique: captura o elemento e abre campo pra sua instrução.
|
|
20
|
+
* - Painel flutuante (canto inferior direito): lista, edita, remove itens.
|
|
21
|
+
* - "Copiar backlog": gera markdown numerado no clipboard, pronto pra colar
|
|
22
|
+
* no Claude Code.
|
|
23
|
+
* - Itens persistem em localStorage (sobrevivem reload e navegação SPA).
|
|
24
|
+
*
|
|
25
|
+
* ESCOPO (POC): sem screenshot, sem extensão, sem backend. Teste primeiro se
|
|
26
|
+
* source mapping + DOM + instrução bastam — provavelmente sim pra layout/estilo.
|
|
27
|
+
*/
|
|
28
|
+
(function () {
|
|
29
|
+
'use strict';
|
|
30
|
+
if (window.__FEEDBACK_COLLECTOR__) return;
|
|
31
|
+
window.__FEEDBACK_COLLECTOR__ = true;
|
|
32
|
+
|
|
33
|
+
var STORAGE_KEY = '__fbc_items_v1';
|
|
34
|
+
|
|
35
|
+
/* ------------------------------------------------------------------ */
|
|
36
|
+
/* Estado */
|
|
37
|
+
/* ------------------------------------------------------------------ */
|
|
38
|
+
var items = load();
|
|
39
|
+
var pickerActive = false;
|
|
40
|
+
var hoverEl = null;
|
|
41
|
+
|
|
42
|
+
function load() {
|
|
43
|
+
try { return JSON.parse(localStorage.getItem(STORAGE_KEY)) || []; }
|
|
44
|
+
catch (e) { return []; }
|
|
45
|
+
}
|
|
46
|
+
function save() {
|
|
47
|
+
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(items)); }
|
|
48
|
+
catch (e) { /* quota — ignora na POC */ }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* ------------------------------------------------------------------ */
|
|
52
|
+
/* Source mapping */
|
|
53
|
+
/* ------------------------------------------------------------------ */
|
|
54
|
+
function getSourceInfo(el) {
|
|
55
|
+
// 1) react-dev-inspector (data attrs no próprio elemento ou ancestral)
|
|
56
|
+
var node = el;
|
|
57
|
+
while (node && node !== document.body) {
|
|
58
|
+
var p = node.getAttribute && (
|
|
59
|
+
node.getAttribute('data-inspector-relative-path') ||
|
|
60
|
+
node.getAttribute('data-source') // convenção alternativa
|
|
61
|
+
);
|
|
62
|
+
if (p) {
|
|
63
|
+
var line = node.getAttribute('data-inspector-line') || '';
|
|
64
|
+
var col = node.getAttribute('data-inspector-column') || '';
|
|
65
|
+
return { file: p, line: line, column: col, via: 'data-attr', ownerHops: node === el ? 0 : 1 };
|
|
66
|
+
}
|
|
67
|
+
node = node.parentElement;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 2) Fallback: React fiber _debugSource (funciona em dev builds do React <19)
|
|
71
|
+
var fiber = getFiber(el);
|
|
72
|
+
var hops = 0;
|
|
73
|
+
while (fiber && hops < 20) {
|
|
74
|
+
var src = fiber._debugSource;
|
|
75
|
+
if (src && src.fileName) {
|
|
76
|
+
return {
|
|
77
|
+
file: relativize(src.fileName),
|
|
78
|
+
line: src.lineNumber || '',
|
|
79
|
+
column: src.columnNumber || '',
|
|
80
|
+
via: 'fiber',
|
|
81
|
+
componentName: fiberName(fiber)
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
fiber = fiber._debugOwner || fiber.return;
|
|
85
|
+
hops++;
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function getFiber(el) {
|
|
91
|
+
for (var k in el) {
|
|
92
|
+
if (k.indexOf('__reactFiber$') === 0 || k.indexOf('__reactInternalInstance$') === 0) {
|
|
93
|
+
return el[k];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function fiberName(fiber) {
|
|
100
|
+
var f = fiber;
|
|
101
|
+
while (f) {
|
|
102
|
+
var t = f.type;
|
|
103
|
+
if (typeof t === 'function') return t.displayName || t.name || '';
|
|
104
|
+
if (t && typeof t === 'object' && t.displayName) return t.displayName;
|
|
105
|
+
f = f._debugOwner || f.return;
|
|
106
|
+
}
|
|
107
|
+
return '';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function componentChain(el) {
|
|
111
|
+
// Nome dos 3 componentes React mais próximos, do mais interno pro externo
|
|
112
|
+
var fiber = getFiber(el);
|
|
113
|
+
var names = [];
|
|
114
|
+
var hops = 0;
|
|
115
|
+
while (fiber && names.length < 3 && hops < 30) {
|
|
116
|
+
var t = fiber.type;
|
|
117
|
+
var name = null;
|
|
118
|
+
if (typeof t === 'function') name = t.displayName || t.name;
|
|
119
|
+
else if (t && typeof t === 'object' && t.displayName) name = t.displayName;
|
|
120
|
+
if (name && names.indexOf(name) === -1) names.push(name);
|
|
121
|
+
fiber = fiber._debugOwner || fiber.return;
|
|
122
|
+
hops++;
|
|
123
|
+
}
|
|
124
|
+
return names;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function relativize(fileName) {
|
|
128
|
+
// Corta caminho absoluto até algo legível (src/, app/, pages/, components/)
|
|
129
|
+
var m = fileName.match(/(?:^|\/)((?:src|app|pages|components|lib|features)\/.*)$/);
|
|
130
|
+
return m ? m[1] : fileName;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/* ------------------------------------------------------------------ */
|
|
134
|
+
/* Seletor CSS estável */
|
|
135
|
+
/* ------------------------------------------------------------------ */
|
|
136
|
+
function cssSelector(el) {
|
|
137
|
+
if (el.id) return '#' + CSS.escape(el.id);
|
|
138
|
+
var parts = [];
|
|
139
|
+
var node = el;
|
|
140
|
+
while (node && node.nodeType === 1 && node !== document.body && parts.length < 5) {
|
|
141
|
+
var part = node.tagName.toLowerCase();
|
|
142
|
+
var stableAttrs = ['data-testid', 'data-test', 'aria-label', 'name'];
|
|
143
|
+
var got = false;
|
|
144
|
+
for (var i = 0; i < stableAttrs.length; i++) {
|
|
145
|
+
var v = node.getAttribute(stableAttrs[i]);
|
|
146
|
+
if (v) { part += '[' + stableAttrs[i] + '="' + v + '"]'; got = true; break; }
|
|
147
|
+
}
|
|
148
|
+
if (!got && node.classList.length) {
|
|
149
|
+
// usa até 2 classes que não pareçam geradas (hash/módulo CSS)
|
|
150
|
+
var cls = Array.prototype.filter.call(node.classList, function (c) {
|
|
151
|
+
return !/^[a-z]*[_-][a-zA-Z0-9]{5,}$/.test(c) && !/^\d/.test(c) && c.length < 30;
|
|
152
|
+
}).slice(0, 2);
|
|
153
|
+
if (cls.length) { part += '.' + cls.map(function(c){ return CSS.escape(c); }).join('.'); got = true; }
|
|
154
|
+
}
|
|
155
|
+
if (!got) {
|
|
156
|
+
var idx = 1, sib = node;
|
|
157
|
+
while ((sib = sib.previousElementSibling)) {
|
|
158
|
+
if (sib.tagName === node.tagName) idx++;
|
|
159
|
+
}
|
|
160
|
+
if (idx > 1) part += ':nth-of-type(' + idx + ')';
|
|
161
|
+
}
|
|
162
|
+
parts.unshift(part);
|
|
163
|
+
// se o seletor parcial já é único, para
|
|
164
|
+
try {
|
|
165
|
+
if (document.querySelectorAll(parts.join(' > ')).length === 1) break;
|
|
166
|
+
} catch (e) { /* seletor inválido, segue */ }
|
|
167
|
+
node = node.parentElement;
|
|
168
|
+
}
|
|
169
|
+
return parts.join(' > ');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/* ------------------------------------------------------------------ */
|
|
173
|
+
/* Computed styles curados */
|
|
174
|
+
/* ------------------------------------------------------------------ */
|
|
175
|
+
var BASE_PROPS = ['display', 'position', 'width', 'height', 'margin', 'padding',
|
|
176
|
+
'color', 'background-color', 'font-size', 'font-weight', 'line-height',
|
|
177
|
+
'border', 'border-radius', 'gap', 'flex-direction', 'justify-content',
|
|
178
|
+
'align-items', 'grid-template-columns', 'overflow', 'z-index', 'opacity',
|
|
179
|
+
'box-shadow', 'text-align'];
|
|
180
|
+
|
|
181
|
+
function curatedStyles(el) {
|
|
182
|
+
var cs = getComputedStyle(el);
|
|
183
|
+
var out = {};
|
|
184
|
+
BASE_PROPS.forEach(function (p) {
|
|
185
|
+
var v = cs.getPropertyValue(p);
|
|
186
|
+
if (!v) return;
|
|
187
|
+
// pula valores default barulhentos
|
|
188
|
+
if (v === 'none' || v === 'normal' || v === 'auto' || v === 'rgba(0, 0, 0, 0)' ||
|
|
189
|
+
v === '0px' || v === 'visible' || v === 'static' || v === 'start') return;
|
|
190
|
+
out[p] = v;
|
|
191
|
+
});
|
|
192
|
+
return out;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function truncatedHTML(el, max) {
|
|
196
|
+
var html = el.outerHTML || '';
|
|
197
|
+
if (html.length <= max) return html;
|
|
198
|
+
// mantém a tag de abertura inteira + começo do conteúdo
|
|
199
|
+
return html.slice(0, max) + '… [truncado]';
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/* ------------------------------------------------------------------ */
|
|
203
|
+
/* Captura */
|
|
204
|
+
/* ------------------------------------------------------------------ */
|
|
205
|
+
function capture(el) {
|
|
206
|
+
var rect = el.getBoundingClientRect();
|
|
207
|
+
var source = getSourceInfo(el);
|
|
208
|
+
var item = {
|
|
209
|
+
id: Date.now() + '_' + Math.random().toString(36).slice(2, 7),
|
|
210
|
+
url: location.pathname + location.search + location.hash,
|
|
211
|
+
tag: el.tagName.toLowerCase(),
|
|
212
|
+
selector: cssSelector(el),
|
|
213
|
+
source: source,
|
|
214
|
+
components: componentChain(el),
|
|
215
|
+
text: (el.textContent || '').trim().slice(0, 120),
|
|
216
|
+
html: truncatedHTML(el, 600),
|
|
217
|
+
styles: curatedStyles(el),
|
|
218
|
+
rect: {
|
|
219
|
+
x: Math.round(rect.x), y: Math.round(rect.y),
|
|
220
|
+
w: Math.round(rect.width), h: Math.round(rect.height)
|
|
221
|
+
},
|
|
222
|
+
viewport: window.innerWidth + 'x' + window.innerHeight,
|
|
223
|
+
instruction: '',
|
|
224
|
+
ts: new Date().toISOString()
|
|
225
|
+
};
|
|
226
|
+
items.push(item);
|
|
227
|
+
save();
|
|
228
|
+
renderPanel();
|
|
229
|
+
// foca o textarea do item recém-criado
|
|
230
|
+
setTimeout(function () {
|
|
231
|
+
var ta = panel.querySelector('[data-fbc-ta="' + item.id + '"]');
|
|
232
|
+
if (ta) ta.focus();
|
|
233
|
+
}, 50);
|
|
234
|
+
flashElement(el);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function flashElement(el) {
|
|
238
|
+
var prev = el.style.outline;
|
|
239
|
+
el.style.outline = '3px solid #16a34a';
|
|
240
|
+
setTimeout(function () { el.style.outline = prev; }, 400);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/* ------------------------------------------------------------------ */
|
|
244
|
+
/* Export markdown */
|
|
245
|
+
/* ------------------------------------------------------------------ */
|
|
246
|
+
function toMarkdown() {
|
|
247
|
+
var lines = ['# Backlog de feedback visual', '',
|
|
248
|
+
'Gerado em ' + new Date().toLocaleString() + ' — ' + items.length + ' itens.',
|
|
249
|
+
'Cada item indica o arquivo:linha quando disponível. Execute na ordem, agrupando edits do mesmo arquivo.', ''];
|
|
250
|
+
items.forEach(function (it, i) {
|
|
251
|
+
lines.push('## ' + (i + 1) + '. ' + (it.instruction ? it.instruction.split('\n')[0] : '(sem instrução)'));
|
|
252
|
+
lines.push('');
|
|
253
|
+
if (it.instruction && it.instruction.indexOf('\n') !== -1) {
|
|
254
|
+
lines.push(it.instruction);
|
|
255
|
+
lines.push('');
|
|
256
|
+
}
|
|
257
|
+
lines.push('- **Rota:** `' + it.url + '`');
|
|
258
|
+
if (it.source) {
|
|
259
|
+
var loc = it.source.file + (it.source.line ? ':' + it.source.line : '');
|
|
260
|
+
lines.push('- **Source:** `' + loc + '`' + (it.source.componentName ? ' (componente `' + it.source.componentName + '`)' : ''));
|
|
261
|
+
} else if (it.components && it.components.length) {
|
|
262
|
+
lines.push('- **Componentes (do mais interno):** ' + it.components.map(function (c) { return '`' + c + '`'; }).join(' → '));
|
|
263
|
+
}
|
|
264
|
+
lines.push('- **Elemento:** `<' + it.tag + '>` — seletor: `' + it.selector + '`');
|
|
265
|
+
if (it.text) lines.push('- **Texto visível:** "' + it.text + '"');
|
|
266
|
+
lines.push('- **Posição:** ' + it.rect.w + 'x' + it.rect.h + ' @ (' + it.rect.x + ',' + it.rect.y + '), viewport ' + it.viewport);
|
|
267
|
+
var styleKeys = Object.keys(it.styles);
|
|
268
|
+
if (styleKeys.length) {
|
|
269
|
+
lines.push('- **Styles atuais:** ' + styleKeys.map(function (k) {
|
|
270
|
+
return '`' + k + ': ' + it.styles[k] + '`';
|
|
271
|
+
}).join(', '));
|
|
272
|
+
}
|
|
273
|
+
lines.push('');
|
|
274
|
+
lines.push('```html');
|
|
275
|
+
lines.push(it.html);
|
|
276
|
+
lines.push('```');
|
|
277
|
+
lines.push('');
|
|
278
|
+
});
|
|
279
|
+
return lines.join('\n');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function copyBacklog() {
|
|
283
|
+
var md = toMarkdown();
|
|
284
|
+
navigator.clipboard.writeText(md).then(function () {
|
|
285
|
+
toast('Backlog copiado (' + items.length + ' itens). Cola no Claude Code.');
|
|
286
|
+
}, function () {
|
|
287
|
+
// fallback: mostra num prompt
|
|
288
|
+
window.prompt('Copie manualmente:', md);
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/* ------------------------------------------------------------------ */
|
|
293
|
+
/* Picker (highlight + clique) */
|
|
294
|
+
/* ------------------------------------------------------------------ */
|
|
295
|
+
var highlight = document.createElement('div');
|
|
296
|
+
highlight.style.cssText = 'position:fixed;pointer-events:none;z-index:2147483645;' +
|
|
297
|
+
'border:2px solid #2563eb;background:rgba(37,99,235,.08);border-radius:2px;' +
|
|
298
|
+
'display:none;transition:all .05s linear;';
|
|
299
|
+
var highlightLabel = document.createElement('div');
|
|
300
|
+
highlightLabel.style.cssText = 'position:absolute;top:-22px;left:0;background:#2563eb;' +
|
|
301
|
+
'color:#fff;font:11px/1.4 ui-monospace,monospace;padding:1px 6px;border-radius:2px;white-space:nowrap;';
|
|
302
|
+
highlight.appendChild(highlightLabel);
|
|
303
|
+
|
|
304
|
+
function onKeyDown(e) {
|
|
305
|
+
if (e.key === 'Alt' && !pickerActive) {
|
|
306
|
+
pickerActive = true;
|
|
307
|
+
document.body.style.cursor = 'crosshair';
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
function onKeyUp(e) {
|
|
311
|
+
if (e.key === 'Alt') deactivatePicker();
|
|
312
|
+
}
|
|
313
|
+
function deactivatePicker() {
|
|
314
|
+
pickerActive = false;
|
|
315
|
+
hoverEl = null;
|
|
316
|
+
highlight.style.display = 'none';
|
|
317
|
+
document.body.style.cursor = '';
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function onMouseMove(e) {
|
|
321
|
+
if (!pickerActive) return;
|
|
322
|
+
var el = document.elementFromPoint(e.clientX, e.clientY);
|
|
323
|
+
if (!el || panel.contains(el) || el === highlight || highlight.contains(el)) {
|
|
324
|
+
highlight.style.display = 'none';
|
|
325
|
+
hoverEl = null;
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
hoverEl = el;
|
|
329
|
+
var r = el.getBoundingClientRect();
|
|
330
|
+
highlight.style.display = 'block';
|
|
331
|
+
highlight.style.left = r.left + 'px';
|
|
332
|
+
highlight.style.top = r.top + 'px';
|
|
333
|
+
highlight.style.width = r.width + 'px';
|
|
334
|
+
highlight.style.height = r.height + 'px';
|
|
335
|
+
var src = getSourceInfo(el);
|
|
336
|
+
highlightLabel.textContent = '<' + el.tagName.toLowerCase() + '>' +
|
|
337
|
+
(src ? ' ' + src.file.split('/').pop() + (src.line ? ':' + src.line : '') : '');
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function onClick(e) {
|
|
341
|
+
if (!pickerActive || !hoverEl) return;
|
|
342
|
+
if (panel.contains(e.target)) return;
|
|
343
|
+
e.preventDefault();
|
|
344
|
+
e.stopPropagation();
|
|
345
|
+
capture(hoverEl);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/* ------------------------------------------------------------------ */
|
|
349
|
+
/* Painel */
|
|
350
|
+
/* ------------------------------------------------------------------ */
|
|
351
|
+
var panel = document.createElement('div');
|
|
352
|
+
panel.style.cssText = 'position:fixed;bottom:16px;right:16px;width:340px;max-height:70vh;' +
|
|
353
|
+
'z-index:2147483646;background:#1c1c1e;color:#e5e5e7;border-radius:10px;' +
|
|
354
|
+
'box-shadow:0 8px 30px rgba(0,0,0,.4);font:13px/1.45 system-ui,sans-serif;' +
|
|
355
|
+
'display:flex;flex-direction:column;overflow:hidden;';
|
|
356
|
+
|
|
357
|
+
var toastEl = document.createElement('div');
|
|
358
|
+
toastEl.style.cssText = 'position:fixed;bottom:16px;left:50%;transform:translateX(-50%);' +
|
|
359
|
+
'z-index:2147483647;background:#16a34a;color:#fff;padding:8px 16px;border-radius:8px;' +
|
|
360
|
+
'font:13px system-ui,sans-serif;display:none;box-shadow:0 4px 14px rgba(0,0,0,.3);';
|
|
361
|
+
function toast(msg) {
|
|
362
|
+
toastEl.textContent = msg;
|
|
363
|
+
toastEl.style.display = 'block';
|
|
364
|
+
clearTimeout(toastEl.__t);
|
|
365
|
+
toastEl.__t = setTimeout(function () { toastEl.style.display = 'none'; }, 2500);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
var collapsed = false;
|
|
369
|
+
|
|
370
|
+
function renderPanel() {
|
|
371
|
+
var srcCount = items.filter(function (it) { return it.source; }).length;
|
|
372
|
+
panel.innerHTML =
|
|
373
|
+
'<div data-fbc="header" style="display:flex;align-items:center;gap:8px;padding:10px 12px;' +
|
|
374
|
+
'background:#2c2c2e;cursor:pointer;user-select:none;">' +
|
|
375
|
+
'<span style="font-weight:600;">Feedback</span>' +
|
|
376
|
+
'<span style="background:#2563eb;border-radius:10px;padding:0 8px;font-size:11px;">' + items.length + '</span>' +
|
|
377
|
+
(items.length ? '<span style="font-size:11px;color:#8e8e93;">' + srcCount + ' com source</span>' : '') +
|
|
378
|
+
'<span style="margin-left:auto;color:#8e8e93;font-size:11px;">segure ALT + clique</span>' +
|
|
379
|
+
'<span data-fbc="toggle" style="color:#8e8e93;">' + (collapsed ? '▸' : '▾') + '</span>' +
|
|
380
|
+
'</div>' +
|
|
381
|
+
(collapsed ? '' :
|
|
382
|
+
'<div style="overflow-y:auto;flex:1;padding:8px 12px;display:flex;flex-direction:column;gap:8px;">' +
|
|
383
|
+
(items.length === 0
|
|
384
|
+
? '<div style="color:#8e8e93;padding:12px 0;text-align:center;">Nenhum item. Segure ALT e clique num elemento da página.</div>'
|
|
385
|
+
: items.map(renderItem).join('')) +
|
|
386
|
+
'</div>' +
|
|
387
|
+
'<div style="display:flex;gap:8px;padding:10px 12px;background:#2c2c2e;">' +
|
|
388
|
+
'<button data-fbc="copy" style="flex:1;background:#2563eb;color:#fff;border:0;border-radius:6px;' +
|
|
389
|
+
'padding:8px;font:600 13px system-ui;cursor:pointer;"' + (items.length ? '' : ' disabled style="flex:1;background:#3a3a3c;color:#8e8e93;border:0;border-radius:6px;padding:8px;font:600 13px system-ui;"') + '>Copiar backlog</button>' +
|
|
390
|
+
'<button data-fbc="clear" style="background:#3a3a3c;color:#e5e5e7;border:0;border-radius:6px;' +
|
|
391
|
+
'padding:8px 12px;font:13px system-ui;cursor:pointer;">Limpar</button>' +
|
|
392
|
+
'</div>');
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function esc(s) {
|
|
396
|
+
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/"/g, '"');
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function renderItem(it, i) {
|
|
400
|
+
var loc = it.source
|
|
401
|
+
? it.source.file.split('/').slice(-2).join('/') + (it.source.line ? ':' + it.source.line : '')
|
|
402
|
+
: (it.components && it.components[0] ? '<' + it.components[0] + '>' : it.selector.slice(0, 40));
|
|
403
|
+
return '<div style="background:#2c2c2e;border-radius:8px;padding:8px;">' +
|
|
404
|
+
'<div style="display:flex;align-items:baseline;gap:6px;margin-bottom:6px;">' +
|
|
405
|
+
'<span style="color:#8e8e93;font-size:11px;">#' + (i + 1) + '</span>' +
|
|
406
|
+
'<span style="font-family:ui-monospace,monospace;font-size:11px;color:#64d2ff;overflow:hidden;' +
|
|
407
|
+
'text-overflow:ellipsis;white-space:nowrap;flex:1;" title="' + esc(it.selector) + '">' + esc(loc) + '</span>' +
|
|
408
|
+
'<span data-fbc-del="' + it.id + '" style="cursor:pointer;color:#8e8e93;padding:0 4px;" title="Remover">✕</span>' +
|
|
409
|
+
'</div>' +
|
|
410
|
+
'<textarea data-fbc-ta="' + it.id + '" placeholder="O que mudar aqui?" ' +
|
|
411
|
+
'style="width:100%;box-sizing:border-box;background:#1c1c1e;color:#e5e5e7;border:1px solid #3a3a3c;' +
|
|
412
|
+
'border-radius:6px;padding:6px;font:13px system-ui;resize:vertical;min-height:40px;">' +
|
|
413
|
+
esc(it.instruction) + '</textarea>' +
|
|
414
|
+
'</div>';
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
panel.addEventListener('click', function (e) {
|
|
418
|
+
var t = e.target;
|
|
419
|
+
if (t.getAttribute('data-fbc') === 'copy') { copyBacklog(); return; }
|
|
420
|
+
if (t.getAttribute('data-fbc') === 'clear') {
|
|
421
|
+
if (confirm('Limpar todos os ' + items.length + ' itens?')) {
|
|
422
|
+
items = []; save(); renderPanel();
|
|
423
|
+
}
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
if (t.hasAttribute('data-fbc-del')) {
|
|
427
|
+
var id = t.getAttribute('data-fbc-del');
|
|
428
|
+
items = items.filter(function (it) { return it.id !== id; });
|
|
429
|
+
save(); renderPanel();
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
var header = t.closest('[data-fbc="header"]');
|
|
433
|
+
if (header) { collapsed = !collapsed; renderPanel(); }
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
panel.addEventListener('input', function (e) {
|
|
437
|
+
var t = e.target;
|
|
438
|
+
if (t.hasAttribute('data-fbc-ta')) {
|
|
439
|
+
var id = t.getAttribute('data-fbc-ta');
|
|
440
|
+
var it = items.find(function (x) { return x.id === id; });
|
|
441
|
+
if (it) { it.instruction = t.value; save(); }
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
/* ------------------------------------------------------------------ */
|
|
446
|
+
/* Boot */
|
|
447
|
+
/* ------------------------------------------------------------------ */
|
|
448
|
+
document.addEventListener('keydown', onKeyDown, true);
|
|
449
|
+
document.addEventListener('keyup', onKeyUp, true);
|
|
450
|
+
window.addEventListener('blur', deactivatePicker);
|
|
451
|
+
document.addEventListener('mousemove', onMouseMove, true);
|
|
452
|
+
document.addEventListener('click', onClick, true);
|
|
453
|
+
|
|
454
|
+
document.body.appendChild(highlight);
|
|
455
|
+
document.body.appendChild(panel);
|
|
456
|
+
document.body.appendChild(toastEl);
|
|
457
|
+
renderPanel();
|
|
458
|
+
|
|
459
|
+
console.log('[feedback-collector] ativo. Segure ALT + clique pra capturar. ' +
|
|
460
|
+
items.length + ' item(ns) restaurado(s) do localStorage.');
|
|
461
|
+
})();
|
package/src/index.d.ts
ADDED
package/src/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entry ESM do package. O script em si é um IIFE auto-executável (side effect):
|
|
3
|
+
* importar este módulo injeta o picker/painel na página. Não há API exportada.
|
|
4
|
+
*
|
|
5
|
+
* Guard de SSR: importar este módulo em Node/server (Next SSR, testes) é no-op
|
|
6
|
+
* — o script só carrega quando há `window`. O feedback-collector.js fica sem
|
|
7
|
+
* `export` de propósito, pra também funcionar cru via <script src> em páginas
|
|
8
|
+
* sem bundler (ver "./script" no exports do package.json).
|
|
9
|
+
*/
|
|
10
|
+
if (typeof window !== "undefined") {
|
|
11
|
+
void import("./feedback-collector.js");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export {};
|