ganbatte-os 0.2.37 → 0.2.41
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/.gos/agents/profiles/ganbatte-os-master.md +100 -0
- package/.gos/libraries/caveman-rules.md +58 -0
- package/.gos/libraries/cloudflare-stack-kb.md +161 -0
- package/.gos/libraries/component-reuse-gate.md +75 -0
- package/.gos/libraries/default-stack-kb.md +98 -0
- package/.gos/libraries/engineering-best-practices.md +208 -0
- package/.gos/libraries/gos-compress-setup.md +62 -0
- package/.gos/libraries/intake-questions-mom-test.md +91 -0
- package/.gos/libraries/lucide-icons-policy.md +174 -0
- package/.gos/libraries/security-best-practices.md +138 -0
- package/.gos/libraries/supabase-stack-kb.md +124 -0
- package/.gos/libraries/timer-pattern-spec.md +252 -0
- package/.gos/libraries/typeform-pattern-spec.md +204 -0
- package/.gos/libraries/ui-guardrails-checklist.md +144 -0
- package/.gos/libraries/visual-diff-lenses.md +114 -0
- package/.gos/playbooks/audit-streaming-playbook.md +86 -0
- package/.gos/skills/adr-tech-decisions/SKILL.md +166 -0
- package/.gos/skills/audit-screenshots/SKILL.md +200 -142
- package/.gos/skills/cloudflare-pages-setup/SKILL.md +180 -0
- package/.gos/skills/figma-print-diff/SKILL.md +170 -0
- package/.gos/skills/gos-caveman/SKILL.md +110 -0
- package/.gos/skills/gos-compress/SKILL.md +134 -0
- package/.gos/skills/gos-compress/scripts/compress.py +346 -0
- package/.gos/skills/gos-compress/scripts/setup.py +91 -0
- package/.gos/skills/idea-intake/SKILL.md +147 -0
- package/.gos/skills/plan-blueprint/SKILL.md +18 -3
- package/.gos/skills/plan-to-tasks/SKILL.md +37 -1
- package/.gos/skills/prd-from-intake/SKILL.md +94 -0
- package/.gos/skills/prototype-orchestrator/SKILL.md +120 -0
- package/.gos/skills/registry.json +12 -1
- package/.gos/skills/timer-component-pattern/SKILL.md +245 -0
- package/.gos/skills/typeform-form-pattern/SKILL.md +210 -0
- package/.gos/skills/ui-guardrails/SKILL.md +111 -0
- package/.gos/templates/intakeTemplate.md +41 -0
- package/.gos/templates/planTemplate.md +25 -4
- package/.gos/templates/prdLeanTemplate.md +40 -0
- package/.gos/templates/taskTemplate.md +29 -5
- package/package.json +1 -1
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Supabase Stack KB — Free Tier (2026-05)
|
|
2
|
+
|
|
3
|
+
> Referencia consultada por `adr-tech-decisions`. Data: 2026-05.
|
|
4
|
+
> Sempre verificar https://supabase.com/pricing para limites atuais.
|
|
5
|
+
|
|
6
|
+
## Filosofia G-OS para Supabase
|
|
7
|
+
|
|
8
|
+
1. Usar quando precisa Postgres real, auth pronta, ou Realtime escalavel.
|
|
9
|
+
2. Combina bem com Cloudflare Pages (front) + Supabase (backend completo).
|
|
10
|
+
3. Free tier projetos pausam apos 7 dias inativos — atencao para MVPs descartaveis em demo.
|
|
11
|
+
4. **Anti-vendor-lock**: Postgres standard, Auth pode ser substituida (Auth.js, Lucia).
|
|
12
|
+
|
|
13
|
+
## Free tier (2 projetos ativos simultaneos)
|
|
14
|
+
|
|
15
|
+
### Database (Postgres)
|
|
16
|
+
- 500MB storage
|
|
17
|
+
- 2GB bandwidth/mes
|
|
18
|
+
- 50k MAU em Auth
|
|
19
|
+
- 5GB storage de arquivos (Storage)
|
|
20
|
+
- 7 dias de inatividade -> projeto pausa (precisa logar e dar resume)
|
|
21
|
+
|
|
22
|
+
### Auth (free 50k MAU)
|
|
23
|
+
- Email/password, magic link, OAuth (Google, GitHub, ~20 providers)
|
|
24
|
+
- Row Level Security (RLS) Postgres-based
|
|
25
|
+
- JWT signing automatico
|
|
26
|
+
- **Limite: 4 social providers em free, 30 em pro**
|
|
27
|
+
|
|
28
|
+
### Realtime (free)
|
|
29
|
+
- Postgres logical replication -> canais por tabela
|
|
30
|
+
- 200 conexoes simultaneas
|
|
31
|
+
- 2M mensagens/mes
|
|
32
|
+
- **Limite: 200 conexoes pode ser baixo para chat publico; suficiente para dashboard interno**
|
|
33
|
+
|
|
34
|
+
### Storage (free)
|
|
35
|
+
- 5GB
|
|
36
|
+
- 5GB egress/mes
|
|
37
|
+
- Image transformations (resize, format)
|
|
38
|
+
|
|
39
|
+
### Edge Functions (free)
|
|
40
|
+
- 500k invocations/mes
|
|
41
|
+
- 2s execution time
|
|
42
|
+
- Deno-based
|
|
43
|
+
- Bom para webhooks, cron jobs, API routes leves
|
|
44
|
+
|
|
45
|
+
## Patterns recomendados
|
|
46
|
+
|
|
47
|
+
### Pattern: Front Cloudflare + Back Supabase (recomendado MVP rapido)
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
[Cloudflare Pages] [Supabase]
|
|
51
|
+
Vite + React Postgres + Auth + Storage
|
|
52
|
+
| |
|
|
53
|
+
+--- supabase-js client ---+
|
|
54
|
+
(direto do browser, RLS protege)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Pros:
|
|
58
|
+
- Sem Workers necessarios
|
|
59
|
+
- Auth pronta em 1 hora
|
|
60
|
+
- RLS substitui boa parte de backend
|
|
61
|
+
|
|
62
|
+
Contras:
|
|
63
|
+
- Vendor lock no Auth (mas Postgres e portavel)
|
|
64
|
+
- Cold start de project apos 7 dias de inatividade
|
|
65
|
+
|
|
66
|
+
### Pattern: Cloudflare Workers + Supabase Postgres (apenas DB)
|
|
67
|
+
|
|
68
|
+
Use quando:
|
|
69
|
+
- Quer logica de servidor (Workers) mas precisa Postgres real (D1 nao basta).
|
|
70
|
+
- Auth simples ou via Cloudflare Access.
|
|
71
|
+
|
|
72
|
+
## Realtime: Supabase Realtime vs Cloudflare DO+WS
|
|
73
|
+
|
|
74
|
+
| Criterio | Supabase Realtime | Cloudflare DO+WS |
|
|
75
|
+
|----------|-------------------|------------------|
|
|
76
|
+
| Custo MVP | Free 200 conn | Free 1M ops/mes |
|
|
77
|
+
| Modelo | Postgres-based (subscribe a INSERT/UPDATE) | Codigo livre (qualquer mensagem) |
|
|
78
|
+
| Boa para | Notificacoes vindas de DB | Chat, jogo, presenca |
|
|
79
|
+
| Limite escala | 200 conn free / 500 pro | 32k por DO, infinito DOs |
|
|
80
|
+
| Latencia | ~100ms | ~50ms (edge) |
|
|
81
|
+
| Setup | `.from('table').on('INSERT', ...)` | Implementar `webSocketMessage()` |
|
|
82
|
+
|
|
83
|
+
**Regra G-OS**: usuario indicou "chat ao vivo" sem DB transacional -> DO+WS. Indicou "notificacao quando alguem cadastra X" -> Supabase Realtime.
|
|
84
|
+
|
|
85
|
+
## Anti-patterns
|
|
86
|
+
|
|
87
|
+
- Free project como demo permanente: pausa em 7 dias, demo quebra.
|
|
88
|
+
- RLS desabilitado em prod: vazamento de dados.
|
|
89
|
+
- service_role key exposta no front: bypassa RLS — NUNCA por no Pages.
|
|
90
|
+
- Storage publico sem moderacao: ataques de upload em MVP.
|
|
91
|
+
|
|
92
|
+
## Bindings (Cloudflare Workers consumindo Supabase)
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
import { createClient } from '@supabase/supabase-js'
|
|
96
|
+
|
|
97
|
+
export default {
|
|
98
|
+
async fetch(req: Request, env: Env) {
|
|
99
|
+
const supabase = createClient(
|
|
100
|
+
env.SUPABASE_URL,
|
|
101
|
+
env.SUPABASE_ANON_KEY // ou SERVICE_ROLE pra ops privilegiadas
|
|
102
|
+
)
|
|
103
|
+
// ...
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`wrangler secret put SUPABASE_URL` e `SUPABASE_ANON_KEY`.
|
|
109
|
+
|
|
110
|
+
## Limites para alarmar
|
|
111
|
+
|
|
112
|
+
| Recurso | Limite free | Alarme |
|
|
113
|
+
|---------|-------------|--------|
|
|
114
|
+
| DB storage | 500MB | >300MB |
|
|
115
|
+
| MAU auth | 50k | >30k |
|
|
116
|
+
| Realtime conn | 200 | >100 sustentadas |
|
|
117
|
+
| Edge Function invocations | 500k/mes | >300k/mes |
|
|
118
|
+
| Inatividade | 7 dias | demos publicas precisam ping |
|
|
119
|
+
|
|
120
|
+
## Ferramentas
|
|
121
|
+
|
|
122
|
+
- **CLI**: `npm i -g supabase` — `supabase init`, `supabase db push`, migrations
|
|
123
|
+
- **Studio**: dashboard web em supabase.com — SQL editor, table editor, Auth users
|
|
124
|
+
- **Local dev**: `supabase start` (precisa Docker) — sobe Postgres + Auth + Storage local
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# Timer Pattern — Spec abstrata
|
|
2
|
+
|
|
3
|
+
> Abstracao do timer-de-estudo passo-a-passo, baseada em `code-kata-timer/script.js`
|
|
4
|
+
> + `code-kata-timer/style.css`. Adaptado para React + TypeScript + Tailwind.
|
|
5
|
+
>
|
|
6
|
+
> Use quando o usuario pedir contador regressivo, timer de pomodoro, code kata,
|
|
7
|
+
> stopwatch com countdown, ou qualquer interface de tempo com play/pause/edit.
|
|
8
|
+
|
|
9
|
+
## State machine
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
export const TimerStatus = {
|
|
13
|
+
STOPPED: 'STOPPED',
|
|
14
|
+
COUNTDOWN: 'COUNTDOWN', // animacao 3-2-1-GO antes de iniciar
|
|
15
|
+
RUNNING: 'RUNNING',
|
|
16
|
+
PAUSED: 'PAUSED',
|
|
17
|
+
EDITING: 'EDITING', // usuario editando valor
|
|
18
|
+
} as const;
|
|
19
|
+
export type TimerStatusType = typeof TimerStatus[keyof typeof TimerStatus];
|
|
20
|
+
|
|
21
|
+
export const isRunning = (s: TimerStatusType) =>
|
|
22
|
+
s === TimerStatus.RUNNING || s === TimerStatus.COUNTDOWN;
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Transicoes validas:
|
|
26
|
+
```
|
|
27
|
+
STOPPED -> COUNTDOWN -> RUNNING -> PAUSED -> RUNNING
|
|
28
|
+
-> STOPPED
|
|
29
|
+
EDITING <-> STOPPED (so edita parado)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Persistencia
|
|
33
|
+
|
|
34
|
+
LocalStorage com chave `timerSettings`:
|
|
35
|
+
```ts
|
|
36
|
+
interface TimerSettings {
|
|
37
|
+
currentSeconds: number;
|
|
38
|
+
timerStatus: TimerStatusType; // sempre STOPPED ao recarregar
|
|
39
|
+
theme: 'dark' | 'light';
|
|
40
|
+
showContributors?: boolean; // app-specific, opcional
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**Regra critica** (do code-kata-timer original): ao carregar settings, **sempre forcar status para STOPPED** — nao restaurar RUNNING. Caso contrario timer "fantasma" continua contando.
|
|
45
|
+
|
|
46
|
+
## Hook reutilizavel
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
export function useTimer(defaultSeconds = 30) {
|
|
50
|
+
const [status, setStatus] = useState<TimerStatusType>(TimerStatus.STOPPED);
|
|
51
|
+
const [seconds, setSeconds] = useState(defaultSeconds);
|
|
52
|
+
const intervalRef = useRef<number | null>(null);
|
|
53
|
+
|
|
54
|
+
const start = useCallback(() => {
|
|
55
|
+
if (status === TimerStatus.RUNNING) return;
|
|
56
|
+
setStatus(TimerStatus.RUNNING);
|
|
57
|
+
intervalRef.current = window.setInterval(() => {
|
|
58
|
+
setSeconds(s => {
|
|
59
|
+
if (s <= 1) {
|
|
60
|
+
if (intervalRef.current) clearInterval(intervalRef.current);
|
|
61
|
+
setStatus(TimerStatus.STOPPED);
|
|
62
|
+
// dispatch event 'timer:done' para componentes ouvirem
|
|
63
|
+
window.dispatchEvent(new CustomEvent('timer:done'));
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
66
|
+
return s - 1;
|
|
67
|
+
});
|
|
68
|
+
}, 1000);
|
|
69
|
+
}, [status]);
|
|
70
|
+
|
|
71
|
+
const pause = useCallback(() => {
|
|
72
|
+
if (intervalRef.current) clearInterval(intervalRef.current);
|
|
73
|
+
setStatus(TimerStatus.PAUSED);
|
|
74
|
+
}, []);
|
|
75
|
+
|
|
76
|
+
const reset = useCallback((to = defaultSeconds) => {
|
|
77
|
+
if (intervalRef.current) clearInterval(intervalRef.current);
|
|
78
|
+
setSeconds(to);
|
|
79
|
+
setStatus(TimerStatus.STOPPED);
|
|
80
|
+
}, [defaultSeconds]);
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
return () => {
|
|
84
|
+
if (intervalRef.current) clearInterval(intervalRef.current);
|
|
85
|
+
};
|
|
86
|
+
}, []);
|
|
87
|
+
|
|
88
|
+
return { status, seconds, start, pause, reset, setSeconds };
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Display
|
|
93
|
+
|
|
94
|
+
Formato `MM:SS` com font monospace (Orbitron, JetBrains Mono ou Tailwind `font-mono`):
|
|
95
|
+
|
|
96
|
+
```tsx
|
|
97
|
+
function formatTime(s: number) {
|
|
98
|
+
const m = Math.floor(s / 60);
|
|
99
|
+
const ss = s % 60;
|
|
100
|
+
return `${String(m).padStart(2, '0')}:${String(ss).padStart(2, '0')}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
<div className="text-7xl md:text-9xl font-mono font-bold tabular-nums">
|
|
104
|
+
{formatTime(seconds)}
|
|
105
|
+
</div>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`tabular-nums` garante que digitos nao "pulem" durante a contagem.
|
|
109
|
+
|
|
110
|
+
## Controles (Lucide React)
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
import { Play, Pause, RotateCcw, Pencil } from "lucide-react";
|
|
114
|
+
|
|
115
|
+
<div className="flex gap-3 justify-center">
|
|
116
|
+
{status === TimerStatus.STOPPED || status === TimerStatus.PAUSED ? (
|
|
117
|
+
<Button onClick={start} size="lg" aria-label="Iniciar">
|
|
118
|
+
<Play className="h-5 w-5" />
|
|
119
|
+
</Button>
|
|
120
|
+
) : (
|
|
121
|
+
<Button onClick={pause} size="lg" aria-label="Pausar">
|
|
122
|
+
<Pause className="h-5 w-5" />
|
|
123
|
+
</Button>
|
|
124
|
+
)}
|
|
125
|
+
<Button onClick={() => reset()} size="lg" variant="outline" aria-label="Resetar">
|
|
126
|
+
<RotateCcw className="h-5 w-5" />
|
|
127
|
+
</Button>
|
|
128
|
+
<Button
|
|
129
|
+
onClick={() => setStatus(TimerStatus.EDITING)}
|
|
130
|
+
size="lg"
|
|
131
|
+
variant="outline"
|
|
132
|
+
aria-label="Editar tempo"
|
|
133
|
+
disabled={isRunning(status)}
|
|
134
|
+
>
|
|
135
|
+
<Pencil className="h-5 w-5" />
|
|
136
|
+
</Button>
|
|
137
|
+
</div>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Atalhos de teclado
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
function onKey(e: KeyboardEvent) {
|
|
145
|
+
if (e.target instanceof HTMLInputElement) return; // nao interferir em editing
|
|
146
|
+
if (e.code === 'Space') { e.preventDefault(); status === TimerStatus.RUNNING ? pause() : start(); }
|
|
147
|
+
if (e.code === 'KeyR') reset();
|
|
148
|
+
if (e.code === 'KeyE') setStatus(TimerStatus.EDITING);
|
|
149
|
+
}
|
|
150
|
+
window.addEventListener('keydown', onKey);
|
|
151
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
152
|
+
}, [status, start, pause, reset]);
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Tema
|
|
156
|
+
|
|
157
|
+
Variaveis CSS (root data-theme):
|
|
158
|
+
- `--timer-bg`: bg principal
|
|
159
|
+
- `--timer-fg`: cor do texto/digitos
|
|
160
|
+
- `--timer-accent`: cor do CTA primario
|
|
161
|
+
|
|
162
|
+
Toggle via `document.documentElement.dataset.theme = 'dark' | 'light'`. Persistir em `localStorage`.
|
|
163
|
+
|
|
164
|
+
## Edicao inline
|
|
165
|
+
|
|
166
|
+
Ao entrar em `EDITING`:
|
|
167
|
+
- Display vira `<input type="number">` com mesmo styling do display.
|
|
168
|
+
- Enter -> salva e volta para `STOPPED`.
|
|
169
|
+
- Esc -> cancela e volta para `STOPPED`.
|
|
170
|
+
|
|
171
|
+
```tsx
|
|
172
|
+
{status === TimerStatus.EDITING ? (
|
|
173
|
+
<input
|
|
174
|
+
type="number"
|
|
175
|
+
autoFocus
|
|
176
|
+
defaultValue={seconds}
|
|
177
|
+
className="text-7xl font-mono font-bold tabular-nums bg-transparent w-64 text-center"
|
|
178
|
+
onBlur={(e) => {
|
|
179
|
+
const v = Number(e.currentTarget.value);
|
|
180
|
+
if (v > 0) setSeconds(v);
|
|
181
|
+
setStatus(TimerStatus.STOPPED);
|
|
182
|
+
}}
|
|
183
|
+
onKeyDown={(e) => {
|
|
184
|
+
if (e.key === 'Enter') e.currentTarget.blur();
|
|
185
|
+
if (e.key === 'Escape') setStatus(TimerStatus.STOPPED);
|
|
186
|
+
}}
|
|
187
|
+
/>
|
|
188
|
+
) : (
|
|
189
|
+
<div>{formatTime(seconds)}</div>
|
|
190
|
+
)}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Eventos customizados
|
|
194
|
+
|
|
195
|
+
Dispatch quando timer termina:
|
|
196
|
+
```ts
|
|
197
|
+
window.dispatchEvent(new CustomEvent('timer:done', { detail: { duration } }));
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Componentes ouvem:
|
|
201
|
+
```ts
|
|
202
|
+
useEffect(() => {
|
|
203
|
+
const handler = (e: Event) => {
|
|
204
|
+
const evt = e as CustomEvent<{ duration: number }>;
|
|
205
|
+
// mostrar toast, tocar som, etc
|
|
206
|
+
};
|
|
207
|
+
window.addEventListener('timer:done', handler);
|
|
208
|
+
return () => window.removeEventListener('timer:done', handler);
|
|
209
|
+
}, []);
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Som ao terminar (opcional)
|
|
213
|
+
|
|
214
|
+
Web Audio API simples — sem precisar arquivo:
|
|
215
|
+
```ts
|
|
216
|
+
function beep(frequency = 800, duration = 200) {
|
|
217
|
+
const ctx = new AudioContext();
|
|
218
|
+
const osc = ctx.createOscillator();
|
|
219
|
+
const gain = ctx.createGain();
|
|
220
|
+
osc.connect(gain);
|
|
221
|
+
gain.connect(ctx.destination);
|
|
222
|
+
osc.frequency.value = frequency;
|
|
223
|
+
osc.start();
|
|
224
|
+
setTimeout(() => { osc.stop(); ctx.close(); }, duration);
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Estados visuais (UI guardrails)
|
|
229
|
+
|
|
230
|
+
- **Loading**: nao aplicavel (timer e estado puro).
|
|
231
|
+
- **Empty**: tempo zerado mostra `00:00` mas botao Iniciar disabled.
|
|
232
|
+
- **Error**: nao deve haver erro em runtime — se houver, fallback para reset.
|
|
233
|
+
- **Running indicator**: Lucide `Activity` pulsando ou borda do display animada.
|
|
234
|
+
|
|
235
|
+
## Anti-patterns
|
|
236
|
+
|
|
237
|
+
- `setInterval` sem cleanup -> memory leak.
|
|
238
|
+
- Restaurar status RUNNING do localStorage -> timer fantasma.
|
|
239
|
+
- Sem `tabular-nums` -> digitos pulam visualmente.
|
|
240
|
+
- Sem atalho de teclado -> mau UX em uso intensivo.
|
|
241
|
+
- Emoji nos botoes -> usar Lucide.
|
|
242
|
+
- Sem dispatch de evento ao terminar -> componentes externos nao reagem.
|
|
243
|
+
|
|
244
|
+
## Reuso no G-OS
|
|
245
|
+
|
|
246
|
+
`gos-master` reconhece pedidos:
|
|
247
|
+
- "timer", "contador regressivo", "countdown"
|
|
248
|
+
- "pomodoro", "pomodoro timer"
|
|
249
|
+
- "code kata timer", "kata"
|
|
250
|
+
- "stopwatch" (variacao — countup vs countdown)
|
|
251
|
+
|
|
252
|
+
Aciona skill `timer-component-pattern` que aplica spec acima.
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# Typeform Pattern — Spec abstrata
|
|
2
|
+
|
|
3
|
+
> Abstracao do quiz/form passo-a-passo estilo Typeform, baseada em
|
|
4
|
+
> `body-metrics-edge/.examples/tsx/old/body-metrics-quiz-typeform.tsx`
|
|
5
|
+
> e `template-lp-wanderson/.a8z/skills/designkit/templates/form.tsx.template`.
|
|
6
|
+
>
|
|
7
|
+
> Use sempre que o usuario pedir form de coleta de dados, quiz, onboarding multi-step,
|
|
8
|
+
> survey, lead form, ou qualquer entrada estruturada que beneficia de **uma pergunta por vez**.
|
|
9
|
+
|
|
10
|
+
## Por que esse pattern
|
|
11
|
+
|
|
12
|
+
- **Conversao 2-3x maior** que forms longos (uma tela por pergunta vs paredao de inputs).
|
|
13
|
+
- **Cognitive load baixo** — usuario ve so uma decisao por vez.
|
|
14
|
+
- **Mobile-first natural** — funciona perfeitamente em telas pequenas.
|
|
15
|
+
- **Estado intermediario salvavel** — refresh nao perde progresso.
|
|
16
|
+
|
|
17
|
+
## Anatomia do pattern
|
|
18
|
+
|
|
19
|
+
### 1. Question schema
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
export type QuestionType = 'welcome' | 'text' | 'number' | 'choice' | 'multiChoice' | 'date' | 'review';
|
|
23
|
+
|
|
24
|
+
export interface QuestionOption {
|
|
25
|
+
value: string;
|
|
26
|
+
label: string;
|
|
27
|
+
description?: string;
|
|
28
|
+
iconName?: keyof typeof import('lucide-react'); // Lucide React (zero emoji)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface Question<F = Record<string, unknown>> {
|
|
32
|
+
id: string;
|
|
33
|
+
type: QuestionType;
|
|
34
|
+
field: keyof F | null;
|
|
35
|
+
question: string | ((data: F) => string); // pode ser dinamica
|
|
36
|
+
description?: string;
|
|
37
|
+
placeholder?: string;
|
|
38
|
+
suffix?: string;
|
|
39
|
+
decimal?: boolean;
|
|
40
|
+
buttonText?: string;
|
|
41
|
+
options?: QuestionOption[];
|
|
42
|
+
validate?: (value: unknown, data: F) => string | null; // null = ok, string = erro
|
|
43
|
+
showWhen?: (data: F) => boolean; // condicional
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 2. State
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
const [currentIndex, setCurrentIndex] = useState(0);
|
|
51
|
+
const [data, setData] = useState<FormData>({});
|
|
52
|
+
const [isAnimating, setIsAnimating] = useState(false);
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Persistir em `sessionStorage` ou `Supabase row` se descartavel = false.
|
|
56
|
+
|
|
57
|
+
### 3. Filtro de ativas (condicional)
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
const activeQuestions = questions.filter(q => !q.showWhen || q.showWhen(data));
|
|
61
|
+
const currentQ = activeQuestions[currentIndex];
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 4. Navegacao
|
|
65
|
+
|
|
66
|
+
- `Enter` -> next (com validacao).
|
|
67
|
+
- `Shift+Enter` (em textarea) -> nova linha.
|
|
68
|
+
- `Esc` -> previous (opcional).
|
|
69
|
+
- Botao "Voltar" sempre visivel exceto na welcome.
|
|
70
|
+
- Animacao opacity+translate-y com 200-300ms entre transicoes.
|
|
71
|
+
|
|
72
|
+
### 5. Componentes-tipo
|
|
73
|
+
|
|
74
|
+
**Welcome screen**
|
|
75
|
+
```
|
|
76
|
+
- Titulo grande (text-4xl/5xl)
|
|
77
|
+
- Subtitulo
|
|
78
|
+
- CTA primario "Comecar" (Button size lg)
|
|
79
|
+
- Lucide icon ilustrativo (h-16 w-16)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Text input**
|
|
83
|
+
```
|
|
84
|
+
- Question (text-2xl semibold)
|
|
85
|
+
- Description opcional (text-base muted-foreground)
|
|
86
|
+
- Input grande (h-12 ou h-14, text-lg)
|
|
87
|
+
- Hint "Pressione Enter para continuar" + CornerDownLeft icon
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Number input**
|
|
91
|
+
```
|
|
92
|
+
- Igual text input mas type="number"
|
|
93
|
+
- Suffix visual ao lado (ex: "anos", "kg", "cm")
|
|
94
|
+
- Decimal opt-in via prop
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Choice (radio)**
|
|
98
|
+
```
|
|
99
|
+
- Stack vertical de cards clicaveis
|
|
100
|
+
- Cada card: Lucide icon + label + description opcional
|
|
101
|
+
- Hover ring + active filled
|
|
102
|
+
- Selecao avanca automaticamente apos 300ms
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**MultiChoice (checkbox)**
|
|
106
|
+
```
|
|
107
|
+
- Igual choice mas selecao acumula
|
|
108
|
+
- Botao "Continuar" obrigatorio (nao auto-avanca)
|
|
109
|
+
- Mostra contagem ("3 de 5 selecionados")
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**Review (final)**
|
|
113
|
+
```
|
|
114
|
+
- Resumo dos dados coletados em cards
|
|
115
|
+
- Botao "Editar" por secao (volta para a question correspondente)
|
|
116
|
+
- CTA primario "Confirmar" / "Calcular" / "Enviar"
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### 6. Animacao
|
|
120
|
+
|
|
121
|
+
```tsx
|
|
122
|
+
<div className={`transition-all duration-300 ${
|
|
123
|
+
isAnimating ? 'opacity-0 translate-y-4' : 'opacity-100 translate-y-0'
|
|
124
|
+
}`}>
|
|
125
|
+
{/* question */}
|
|
126
|
+
</div>
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### 7. Progress bar
|
|
130
|
+
|
|
131
|
+
Top of screen, fixed:
|
|
132
|
+
```tsx
|
|
133
|
+
<div className="fixed top-0 left-0 right-0 h-1 bg-muted">
|
|
134
|
+
<div
|
|
135
|
+
className="h-full bg-primary transition-all duration-300"
|
|
136
|
+
style={{ width: `${((currentIndex + 1) / activeQuestions.length) * 100}%` }}
|
|
137
|
+
/>
|
|
138
|
+
</div>
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### 8. Validacao
|
|
142
|
+
|
|
143
|
+
Por question:
|
|
144
|
+
```ts
|
|
145
|
+
{
|
|
146
|
+
id: 'email',
|
|
147
|
+
type: 'text',
|
|
148
|
+
field: 'email',
|
|
149
|
+
question: 'Qual seu email?',
|
|
150
|
+
validate: (value) => {
|
|
151
|
+
if (!value) return 'Campo obrigatorio';
|
|
152
|
+
if (!/^[^@]+@[^@]+\.[^@]+$/.test(String(value))) return 'Email invalido';
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Erro inline abaixo do input. Nunca toast/alert.
|
|
159
|
+
|
|
160
|
+
### 9. Estados visuais (UI guardrails)
|
|
161
|
+
|
|
162
|
+
- **Loading** (submitting): botao disabled + Lucide `Loader2` com `animate-spin`.
|
|
163
|
+
- **Empty** (review sem dados): mostrar "Voce ainda nao respondeu — comece pelo inicio."
|
|
164
|
+
- **Error** (submit falhou): inline acima do CTA + botao retry.
|
|
165
|
+
- **Success**: tela final com Lucide `CheckCircle2` h-16 w-16 + redirect ou prox-step.
|
|
166
|
+
|
|
167
|
+
### 10. Acessibilidade
|
|
168
|
+
|
|
169
|
+
- `aria-live="polite"` no container de question (anuncia mudanca pra screen reader).
|
|
170
|
+
- Focus auto no input ao mudar de question (`useEffect` + `inputRef.current?.focus()`).
|
|
171
|
+
- Labels via `<Label htmlFor>` ou `aria-label`.
|
|
172
|
+
- Botao voltar com `aria-label="Pergunta anterior"`.
|
|
173
|
+
|
|
174
|
+
## Stack obrigatorio
|
|
175
|
+
|
|
176
|
+
- React 18+
|
|
177
|
+
- TypeScript strict
|
|
178
|
+
- Tailwind v4 + shadcn/ui (Button, Input, Card, Label)
|
|
179
|
+
- Lucide React (UNICA lib de icones — zero emoji)
|
|
180
|
+
- React Hook Form + Zod (validacao schema-first quando form complexo)
|
|
181
|
+
|
|
182
|
+
## Variantes
|
|
183
|
+
|
|
184
|
+
- **Quiz com calculo** (body-metrics-quiz original): apos ultima question, calcula metricas e mostra resultado.
|
|
185
|
+
- **Lead form** (capturar email/whats): submita para Supabase.
|
|
186
|
+
- **Onboarding** (apos signup): salva preferencias na profiles table.
|
|
187
|
+
- **Survey publico** (NPS, feedback): tabela com RLS public-insert apenas.
|
|
188
|
+
|
|
189
|
+
## Anti-patterns
|
|
190
|
+
|
|
191
|
+
- Mostrar todas perguntas de uma vez como form tradicional — defeats the purpose.
|
|
192
|
+
- Sem progress bar — usuario nao sabe quanto falta.
|
|
193
|
+
- Pular validacao no front — mau UX (back so pra forma).
|
|
194
|
+
- Auto-avanco SEM debounce em choice — usuario clica errado e ja avancou.
|
|
195
|
+
- Texto unicode/emoji nas opcoes — usar `iconName` Lucide.
|
|
196
|
+
- Sem estado de submitting no final — usuario clica enviar 3x.
|
|
197
|
+
|
|
198
|
+
## Reuso no G-OS
|
|
199
|
+
|
|
200
|
+
Quando usuario pede "form de cadastro", "quiz", "onboarding", "captura de leads":
|
|
201
|
+
1. `gos-master` sugere pattern typeform.
|
|
202
|
+
2. Usuario aprova -> skill `typeform-form-pattern` gera estrutura base.
|
|
203
|
+
3. Customiza questions[] do dominio.
|
|
204
|
+
4. Estilizacao via tokens shadcn (sem hardcoded colors).
|