ganbatte-os 0.2.37 → 0.2.38
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/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/skills/adr-tech-decisions/SKILL.md +166 -0
- package/.gos/skills/audit-screenshots/SKILL.md +21 -3
- package/.gos/skills/cloudflare-pages-setup/SKILL.md +180 -0
- package/.gos/skills/figma-print-diff/SKILL.md +165 -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 +10 -3
- package/.gos/skills/plan-to-tasks/SKILL.md +28 -0
- 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/prdLeanTemplate.md +40 -0
- package/package.json +1 -1
|
@@ -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).
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# UI Guardrails Checklist (detalhe)
|
|
2
|
+
|
|
3
|
+
> Detalhe expandido de cada item do `ui-guardrails`. Consultar quando uma violacao precisar ser explicada ao usuario.
|
|
4
|
+
|
|
5
|
+
## A — Estados visuais
|
|
6
|
+
|
|
7
|
+
### A1 — Loading
|
|
8
|
+
- Tela inteira: skeleton com a estrutura visual (header skeleton, lista de cards skeleton).
|
|
9
|
+
- Componente isolado (ex: botao salvando): spinner inline ou disabled state.
|
|
10
|
+
- Anti-padrao: tela em branco enquanto carrega.
|
|
11
|
+
|
|
12
|
+
Codegen exige no plan:
|
|
13
|
+
```markdown
|
|
14
|
+
### Estado: loading
|
|
15
|
+
- Skeleton: 3 cards 240x80px com bg-muted-foreground/10 + shimmer.
|
|
16
|
+
- Trigger: query.isPending
|
|
17
|
+
- Duracao max: 3s antes de error.
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### A2 — Empty
|
|
21
|
+
- Copy explicando POR QUE esta vazio.
|
|
22
|
+
- Ilustracao OU icone grande.
|
|
23
|
+
- CTA primario para sair do empty (criar item, importar, etc.).
|
|
24
|
+
- Anti-padrao: "Nenhum dado" sem nada mais.
|
|
25
|
+
|
|
26
|
+
Codegen exige:
|
|
27
|
+
```markdown
|
|
28
|
+
### Estado: empty
|
|
29
|
+
- Copy: "Voce ainda nao tem nenhum projeto. Comece criando um."
|
|
30
|
+
- Icone: FolderPlus 48px text-muted-foreground.
|
|
31
|
+
- CTA: "Criar primeiro projeto" (Button variant=primary).
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### A3 — Error
|
|
35
|
+
- Mensagem clara (nao "erro 500").
|
|
36
|
+
- Acao de recovery (tentar de novo, voltar, contato).
|
|
37
|
+
- Distincao: erro recuperavel vs nao-recuperavel.
|
|
38
|
+
|
|
39
|
+
```markdown
|
|
40
|
+
### Estado: error
|
|
41
|
+
- Tipo recuperavel: toast variant=error + retry inline.
|
|
42
|
+
- Tipo nao-recuperavel: tela full com copy + CTA voltar.
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### A4 — Success (quando aplicavel)
|
|
46
|
+
- Toast inline ou success state pos-form.
|
|
47
|
+
- Auto-dismiss em 3-5s OU acao explicita do usuario.
|
|
48
|
+
|
|
49
|
+
### A5 — Default
|
|
50
|
+
- Estado normal, dados presentes.
|
|
51
|
+
|
|
52
|
+
## B — Responsividade
|
|
53
|
+
|
|
54
|
+
### Breakpoints obrigatorios
|
|
55
|
+
- Mobile: <768px (preferencia: comeca daqui — mobile-first).
|
|
56
|
+
- Tablet: 768-1024px.
|
|
57
|
+
- Desktop: 1024-1920px.
|
|
58
|
+
|
|
59
|
+
### Comportamentos esperados
|
|
60
|
+
- Sidebar -> drawer no mobile.
|
|
61
|
+
- Tabela -> cards no mobile.
|
|
62
|
+
- Drawer right -> bottom sheet no mobile.
|
|
63
|
+
- Tipografia escala (text-base mobile, text-lg desktop ou similar).
|
|
64
|
+
|
|
65
|
+
### Codegen exige no plan
|
|
66
|
+
```markdown
|
|
67
|
+
### Responsividade
|
|
68
|
+
- Mobile: lista vira cards verticais empilhados, sem hover.
|
|
69
|
+
- Tablet: 2 colunas de cards.
|
|
70
|
+
- Desktop: tabela tradicional com hover.
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## C — Acessibilidade
|
|
74
|
+
|
|
75
|
+
### Roles ARIA
|
|
76
|
+
- `<button>` para acao -> nao usar `<div onClick>`.
|
|
77
|
+
- `<dialog>` ou `role="dialog"` + aria-labelledby para modals.
|
|
78
|
+
- `role="listbox"` + `role="option"` em selects custom.
|
|
79
|
+
|
|
80
|
+
### Labels
|
|
81
|
+
- Input sem `<label>` visivel: usar `aria-label`.
|
|
82
|
+
- Icon-only button: `aria-label="Fechar"` obrigatorio.
|
|
83
|
+
|
|
84
|
+
### Focus
|
|
85
|
+
- Modal aberto: focus vai para primeiro elemento focavel.
|
|
86
|
+
- Modal fechado: focus volta para trigger.
|
|
87
|
+
- Tab order segue ordem visual.
|
|
88
|
+
- Focus indicator distinto de hover (outline 2px ring-primary).
|
|
89
|
+
|
|
90
|
+
### Contraste
|
|
91
|
+
- AA: 4.5:1 texto, 3:1 UI.
|
|
92
|
+
- Erro inline NAO so vermelho — adicionar icone/texto "Erro:".
|
|
93
|
+
|
|
94
|
+
## D — Tokens do DS
|
|
95
|
+
|
|
96
|
+
### Cores
|
|
97
|
+
- OK: `bg-primary`, `text-foreground`, `border-border`.
|
|
98
|
+
- Anti: `bg-[#3b82f6]`, `text-blue-500` quando primary existe.
|
|
99
|
+
|
|
100
|
+
### Spacing
|
|
101
|
+
- OK: `gap-2`, `p-4`, `mt-6`.
|
|
102
|
+
- Anti: `gap-[7px]`, `p-[13px]`.
|
|
103
|
+
|
|
104
|
+
### Typography
|
|
105
|
+
- OK: `text-sm`, `font-semibold`, `leading-tight`.
|
|
106
|
+
- Anti: `text-[13px]`, `tracking-[0.02em]` quando token existe.
|
|
107
|
+
|
|
108
|
+
### Radius
|
|
109
|
+
- OK: `rounded`, `rounded-md`, `rounded-lg`.
|
|
110
|
+
- Anti: `rounded-[7px]`.
|
|
111
|
+
|
|
112
|
+
### Excecoes permitidas (com justificativa)
|
|
113
|
+
- Animacao especifica nao-mapeada: `[animation-delay:120ms]` OK.
|
|
114
|
+
- Posicionamento dinamico: `top-[calc(100%+8px)]` OK.
|
|
115
|
+
|
|
116
|
+
## E — Interacao
|
|
117
|
+
|
|
118
|
+
### Trigger declaration
|
|
119
|
+
Cada elemento interativo precisa:
|
|
120
|
+
- Trigger: o que dispara (click, hover, focus, keypress, mount, query)
|
|
121
|
+
- Acao: que callback/mutation roda
|
|
122
|
+
- Resultado: qual estado muda + UI feedback
|
|
123
|
+
|
|
124
|
+
### Edge cases obrigatorios
|
|
125
|
+
- Double-click rapido em CTA -> debounce 300ms ou disabled durante mutation.
|
|
126
|
+
- Mutation lenta (>3s) -> mostrar loading inline ou progress.
|
|
127
|
+
- Mutation falha -> rollback ou error state com retry.
|
|
128
|
+
- Concorrencia (2 usuarios editando) -> conflict resolution declarado.
|
|
129
|
+
|
|
130
|
+
## Self-check rapido
|
|
131
|
+
|
|
132
|
+
Antes de submeter tela para codegen:
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
[ ] A1 loading state declarado e visualmente coerente?
|
|
136
|
+
[ ] A2 empty state existe + CTA primaria?
|
|
137
|
+
[ ] A3 error state com recovery?
|
|
138
|
+
[ ] B responsividade nos 3 breakpoints?
|
|
139
|
+
[ ] C aria-label em icon buttons + focus visivel?
|
|
140
|
+
[ ] D 0 valores hardcoded fora do DS?
|
|
141
|
+
[ ] E todos os triggers tem acao + resultado + edge cases?
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Falhou em algum -> NAO codar ainda. Volte ao plano.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Visual Diff Lenses
|
|
2
|
+
|
|
3
|
+
> Referencia das 6 lenses usadas por `figma-print-diff` e `audit-screenshots`. Inspirado em Nielsen Heuristics + design-critique (uxdudu) + WCAG.
|
|
4
|
+
|
|
5
|
+
## Lens 1 — Layout & hierarquia
|
|
6
|
+
|
|
7
|
+
Pergunta-mestra: "A estrutura espacial bate?"
|
|
8
|
+
|
|
9
|
+
Sub-checks:
|
|
10
|
+
- Containers principais (sidebar, header, footer, main) presentes em ambos?
|
|
11
|
+
- Ordem de filhos dentro de cada container coincide?
|
|
12
|
+
- Proporcoes (sidebar 240px, main 1fr) coerentes?
|
|
13
|
+
- Densidade visual (cards/lista) similar?
|
|
14
|
+
|
|
15
|
+
Severity guide:
|
|
16
|
+
- high: container ausente/extra
|
|
17
|
+
- medium: ordem trocada
|
|
18
|
+
- low: proporcao 10-20% diferente
|
|
19
|
+
|
|
20
|
+
## Lens 2 — Tokens visuais
|
|
21
|
+
|
|
22
|
+
Pergunta-mestra: "Cor, tipo, espacamento batem com o DS?"
|
|
23
|
+
|
|
24
|
+
Sub-checks:
|
|
25
|
+
- Cor de background dos blocos
|
|
26
|
+
- Cor de texto (primary, secondary, muted)
|
|
27
|
+
- Borders e radius
|
|
28
|
+
- Tipografia (family, size, weight, line-height)
|
|
29
|
+
- Spacing (padding/margin/gap)
|
|
30
|
+
- Shadows e elevations
|
|
31
|
+
|
|
32
|
+
Severity guide:
|
|
33
|
+
- high: cor de acao primaria errada
|
|
34
|
+
- medium: spacing 2x diferente
|
|
35
|
+
- low: radius cosmetico
|
|
36
|
+
|
|
37
|
+
## Lens 3 — Estados
|
|
38
|
+
|
|
39
|
+
Pergunta-mestra: "Todos os estados estao implementados?"
|
|
40
|
+
|
|
41
|
+
Sub-checks:
|
|
42
|
+
- Loading (skeleton, spinner)
|
|
43
|
+
- Empty (placeholder, CTA)
|
|
44
|
+
- Error (mensagem, recovery)
|
|
45
|
+
- Success (toast, inline)
|
|
46
|
+
- Hover/Focus/Active
|
|
47
|
+
- Disabled
|
|
48
|
+
|
|
49
|
+
Severity guide:
|
|
50
|
+
- high: empty/error nao implementado
|
|
51
|
+
- medium: hover sem feedback
|
|
52
|
+
- low: focus ring cosmetico
|
|
53
|
+
|
|
54
|
+
## Lens 4 — Conteudo
|
|
55
|
+
|
|
56
|
+
Pergunta-mestra: "Textos e visuais coincidem?"
|
|
57
|
+
|
|
58
|
+
Sub-checks:
|
|
59
|
+
- Textos literais (sem inventar traducao)
|
|
60
|
+
- Icones (mesma lib, mesma variante)
|
|
61
|
+
- Imagens (placeholder vs real)
|
|
62
|
+
- Dados mockados (formato, plausibilidade)
|
|
63
|
+
|
|
64
|
+
Severity guide:
|
|
65
|
+
- high: copy errada que altera significado
|
|
66
|
+
- medium: icone trocado
|
|
67
|
+
- low: lorem ipsum esquecido
|
|
68
|
+
|
|
69
|
+
## Lens 5 — Interacao (inferida do estatico)
|
|
70
|
+
|
|
71
|
+
Pergunta-mestra: "Affordances batem?"
|
|
72
|
+
|
|
73
|
+
Sub-checks:
|
|
74
|
+
- Cursor pointer onde Figma mostra elemento "clicavel"
|
|
75
|
+
- Botoes com aparencia de botao (nao texto plano)
|
|
76
|
+
- Inputs com aparencia de input (border, placeholder)
|
|
77
|
+
- Tooltips/popovers posicionados corretamente quando visiveis
|
|
78
|
+
|
|
79
|
+
Severity guide:
|
|
80
|
+
- high: botao primario sem aparencia de botao
|
|
81
|
+
- medium: link disfarcado de texto
|
|
82
|
+
- low: cursor em element nao-clicavel
|
|
83
|
+
|
|
84
|
+
## Lens 6 — Acessibilidade visual
|
|
85
|
+
|
|
86
|
+
Pergunta-mestra: "Acessivel ao olhar?"
|
|
87
|
+
|
|
88
|
+
Sub-checks:
|
|
89
|
+
- Contraste AA (4.5:1 texto, 3:1 UI components)
|
|
90
|
+
- Toque minimo 44x44px em mobile
|
|
91
|
+
- Focus indicator distinto de hover
|
|
92
|
+
- Texto sem so cor (icone + cor para indicar erro)
|
|
93
|
+
|
|
94
|
+
Severity guide:
|
|
95
|
+
- high: contraste reprovado em texto principal
|
|
96
|
+
- medium: toque <44px em mobile
|
|
97
|
+
- low: focus mesmo cor que hover
|
|
98
|
+
|
|
99
|
+
## Composicao das lenses
|
|
100
|
+
|
|
101
|
+
Ordem fixa: 1 -> 2 -> 3 -> 4 -> 5 -> 6.
|
|
102
|
+
|
|
103
|
+
Por que essa ordem?
|
|
104
|
+
- Layout antes de token (sem layout, token nao importa).
|
|
105
|
+
- Token antes de estado (estado depende de tokens corretos).
|
|
106
|
+
- Estado antes de conteudo (conteudo so faz sentido com estado completo).
|
|
107
|
+
- Interacao depois de conteudo (precisa ver afford).
|
|
108
|
+
- A11y por ultimo (gate final).
|
|
109
|
+
|
|
110
|
+
## Anti-patterns
|
|
111
|
+
|
|
112
|
+
- Pular lens 3: time esquece estados, codegen sai sem loading/empty/error.
|
|
113
|
+
- Inverter 1 e 2: comeca medindo cor e nao percebe que falta sidebar inteira.
|
|
114
|
+
- Severity hyperinflation: tudo high -> nada e high.
|