Compare commits
20 Commits
63fd584c79
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 0303870669 | |||
| 9d52098b57 | |||
| 471b209a8a | |||
| e428de8df5 | |||
| eb72ec72d9 | |||
| b49ed7c257 | |||
| 1d26482872 | |||
| 13421c4bf0 | |||
| adeb4dad33 | |||
| 8aa77a5ac3 | |||
| dc16c7105a | |||
| 46ce6b67a2 | |||
| 6e82828b53 | |||
| 51a806b506 | |||
| ddb2c93c7f | |||
| 0028321019 | |||
| 0a426e00d9 | |||
| 6b6c706979 | |||
| e8342a1c30 | |||
| cfb037d081 |
+7
-7
@@ -3,31 +3,31 @@
|
|||||||
"skills": {
|
"skills": {
|
||||||
"agent-browser-clawdbot": {
|
"agent-browser-clawdbot": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"installedAt": 1779243267499
|
"installedAt": 1779300377917
|
||||||
},
|
},
|
||||||
"vision": {
|
"vision": {
|
||||||
"version": "3.5.0",
|
"version": "3.5.0",
|
||||||
"installedAt": 1779243291577
|
"installedAt": 1779300380127
|
||||||
},
|
},
|
||||||
"self-improvement": {
|
"self-improvement": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"installedAt": 1779243314748
|
"installedAt": 1779300383146
|
||||||
},
|
},
|
||||||
"multi-search-engine-2-0-1": {
|
"multi-search-engine-2-0-1": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"installedAt": 1779243337647
|
"installedAt": 1779300386960
|
||||||
},
|
},
|
||||||
"redis-labs-integration": {
|
"redis-labs-integration": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"installedAt": 1779243351883
|
"installedAt": 1779300388864
|
||||||
},
|
},
|
||||||
"nova-self-improver": {
|
"nova-self-improver": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"installedAt": 1779233727234
|
"installedAt": 1779300391129
|
||||||
},
|
},
|
||||||
"typescript": {
|
"typescript": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"installedAt": 1779234199578
|
"installedAt": 1779300393477
|
||||||
},
|
},
|
||||||
"xcloud-docker-deploy": {
|
"xcloud-docker-deploy": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
|
|||||||
@@ -228,3 +228,169 @@ Workaround mínimo para testar onChange em inputs no jsdom puro, onde `.value`
|
|||||||
- Tags: jsdom, fireEvent, input, value
|
- Tags: jsdom, fireEvent, input, value
|
||||||
- Pattern-Key: jsdom.fireEvent-change-writable
|
- Pattern-Key: jsdom.fireEvent-change-writable
|
||||||
- Recurrence-Count: 1
|
- Recurrence-Count: 1
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [LRN-20260520-006] portainer-api-ptr-token-scope
|
||||||
|
|
||||||
|
**Logged**: 2026-05-20T17:38:00-03:00
|
||||||
|
**Priority**: high
|
||||||
|
**Status**: reference
|
||||||
|
**Area**: devops | infra | docker
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
Token Portainer com prefixo `ptr_` é um access token — funciona em `/api/status` (200) mas retorna 401 em `/api/endpoints`, `/api/stacks`, `/api/auth` — escopo muito limitado.
|
||||||
|
|
||||||
|
### Details
|
||||||
|
Para operações de mutação (criar/gerenciar stacks) pela API Portainer é necessário JWT admin válido obtido via `POST /api/auth` com username+senha. Senha admin do Portainer não estava documentada — usamos `docker stack deploy` via CLI Docker Swarm diretamente como workaround.
|
||||||
|
|
||||||
|
### Suggested Action
|
||||||
|
Documentar senha admin do Portainer de forma segura (password manager), ou usar service token JWT com escopo `admin` completo.
|
||||||
|
|
||||||
|
### Metadata
|
||||||
|
- Source: error
|
||||||
|
- Tags: portainer, api, token, docker-swarm, jwt
|
||||||
|
- Pattern-Key: portainer.api-ptr-token-scope
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [LRN-20260520-007] docker-compose-v3-swarm-labels-and-restart-policy
|
||||||
|
|
||||||
|
**Logged**: 2026-05-20T17:38:00-03:00
|
||||||
|
**Priority**: high
|
||||||
|
**Status**: reference
|
||||||
|
**Area**: devops | docker
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
docker stack deploy com compose v3.9 no Swarm mode — duas propriedades que não funcionam como esperado:
|
||||||
|
|
||||||
|
1. **`restart_policy`** — não é propriedade válida no `deploy` spec; Docker Swarm gerencia restart nativamente (Always)
|
||||||
|
2. **`deploy.labels`** — não se tornam container labels; labels só são aplicadas via `docker service create --label-add` ou `--label-add` em `docker service update`
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
- Remover `restart_policy` do YAML
|
||||||
|
- Aplicar labels necessárias (ex: caddy=) via CLI `docker service update --label-add` ou diretamente no `docker service create`
|
||||||
|
|
||||||
|
### Metadata
|
||||||
|
- Source: error
|
||||||
|
- Tags: docker, swarm, compose, labels, restart-policy
|
||||||
|
- Pattern-Key: docker.swarm-compose-v3-gotchas
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [LRN-20260520-008] stack-migration-docker-swarm-createmigrate
|
||||||
|
|
||||||
|
**Logged**: 2026-05-20T17:38:00-03:00
|
||||||
|
**Priority**: medium
|
||||||
|
**Status**: reference
|
||||||
|
**Area**: devops | docker
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
Migração de stack Docker Swarm (trocar de nome/namespace):
|
||||||
|
1. Criar novos serviços com o nome da stack destino + labels corretas (`docker service create --label-add ...`)
|
||||||
|
2. Validar que os serviços estão saudáveis
|
||||||
|
3. Remover a stack antiga (`docker stack rm <antiga>`)
|
||||||
|
|
||||||
|
### Safety notes
|
||||||
|
- Não usar `docker stack deploy --prune` em stack ativa em produção — apaga services não presentes no novo compose
|
||||||
|
- Para imagens locais: usar `--with-registry-auth` ou tag com registry acessível
|
||||||
|
- Sempre validar `docker service ps <nome>` antes de remover a stack antiga
|
||||||
|
|
||||||
|
### Metadata
|
||||||
|
- Source: best_practice
|
||||||
|
- Tags: docker, swarm, migration, stack
|
||||||
|
- Pattern-Key: docker.swarm-stack-migration
|
||||||
|
|
||||||
|
|
||||||
|
## LRN-20260520-010 — Stack Portainer API Attachable=false bloqueia deploy
|
||||||
|
|
||||||
|
**Categoria**: knowledge_gap
|
||||||
|
|
||||||
|
### Problema
|
||||||
|
- Rede `public` tem `Attachable: false`
|
||||||
|
- `docker stack deploy` CLI FUNCIONA mesmo assim (Daemon local ignora)
|
||||||
|
- Portainer API BLOQUEIA (Docker Remote API respeita `Attachable=false`)
|
||||||
|
- Erro: `failed to set up container networking: Could not attach to network public: rpc error: code = PermissionDenied desc = network public not manually attachable`
|
||||||
|
|
||||||
|
### Decisão tomada
|
||||||
|
Stack `project` fica gerenciada por `docker stack deploy` CLI (Swarm nativo).
|
||||||
|
Portainer controla apenas stacks 1,2,4,6,7,8,9 (com SwarmId preenchido).
|
||||||
|
Stack `project` não é controlável pelo Portainer enquanto `Attachable=false`.
|
||||||
|
|
||||||
|
### Para resolver completamente
|
||||||
|
Recriar rede `public` com `--attachable` impactando 19 containers — NÃO recomendado agora.
|
||||||
|
|
||||||
|
|
||||||
|
## [LRN-20260520-011] atomic-design-3d-threejs-landing-page
|
||||||
|
|
||||||
|
**Logged**: 2026-05-20T20:00:00-03:00
|
||||||
|
**Priority**: high
|
||||||
|
**Status**: reference
|
||||||
|
**Area**: frontend | web3d | design-system
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
Landing page 3D completa usando Atomic Design + Three.js + React Three Fiber + Framer Motion — código funcional com build OK.
|
||||||
|
|
||||||
|
### Padrão aplicado
|
||||||
|
```
|
||||||
|
Átomos (11) → Moléculas (3) → Organismos (2) → Templates (2) → Pages (1)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tokens 8 domínios**: space / font / color / shadow / radius / material3d / camera3d / animation
|
||||||
|
|
||||||
|
### Stack
|
||||||
|
- Vite + React 18 + TS
|
||||||
|
- @react-three/fiber + drei
|
||||||
|
- framer-motion
|
||||||
|
- Build: `npm install && npm run build` ✅
|
||||||
|
|
||||||
|
### Decisões
|
||||||
|
- Canvas fullscreen com overlay 2D (SceneCanvas + ThreePage template)
|
||||||
|
- ScrollControls → scroll move câmera 3D
|
||||||
|
- Material tokens: roughness 0.08 + metalness 0.75 = super-reflexivo
|
||||||
|
- PointLights accent + secondary por cena
|
||||||
|
- CSS vars para dark/light mode + A11y
|
||||||
|
|
||||||
|
### Hot Reload (future)
|
||||||
|
- `npm run dev` → Vite HMR
|
||||||
|
- Adicionar ao stack Swarm `dev` (volume bind mount + dnsrr)
|
||||||
|
|
||||||
|
|
||||||
|
## [LRN-20260520-012] proxy-test-octal-deploy-zero-downtime
|
||||||
|
|
||||||
|
**Logged**: 2026-05-20T20:15:00-03:00
|
||||||
|
**Priority**: high
|
||||||
|
**Status**: reference
|
||||||
|
**Area**: devops | docker | caddy
|
||||||
|
|
||||||
|
### Problema
|
||||||
|
test.octal.tec.br estava com HTML estático antigo (Octal Technology landing page). Precisou atualizar para o Pulse 3D.
|
||||||
|
|
||||||
|
### Solução aplicada
|
||||||
|
1. Extrair HTML atual da imagem `test-octal:latest` via `docker cp`
|
||||||
|
2. Substituir pelo HTML novo (Pulse 3D — CSS completo)
|
||||||
|
3. Build: `docker build -t test-octal:latest .` no diretório com HTML + Dockerfile
|
||||||
|
4. Deploy: `docker service update --image test-octal:latest proxy_test-octal`
|
||||||
|
5. Resultado: zero-downtime, serviço convergiu com imagem nova
|
||||||
|
|
||||||
|
### Padrão
|
||||||
|
```bash
|
||||||
|
# Extrair atual
|
||||||
|
mkdir /tmp/site-src && docker cp container:/usr/share/nginx/html . /tmp/site-src/
|
||||||
|
# Atualizar index.html
|
||||||
|
cp novo.html /tmp/site-src/index.html
|
||||||
|
# Reconstruir
|
||||||
|
cd /tmp/site-src && cat > Dockerfile << 'D'
|
||||||
|
FROM nginx:alpine
|
||||||
|
COPY index.html /usr/share/nginx/html/index.html
|
||||||
|
EXPOSE 80
|
||||||
|
CMD ["nginx","-g","daemon off;"]
|
||||||
|
D
|
||||||
|
docker build -t test-octal:latest . && docker push registry.octal.tec.br/pulse/test-octal:latest
|
||||||
|
# Atualizar service
|
||||||
|
docker service update --image test-octal:latest proxy_test-octal
|
||||||
|
```
|
||||||
|
|
||||||
|
### Resultado
|
||||||
|
- https://test.octal.tec.br agora mostra Pulse 3D Landing
|
||||||
|
- Registry push pode falhar com SSL (registry local com cert autoassinado) — usar imagem local diretamente no `docker service update` funciona
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
FROM nginx:alpine
|
||||||
|
COPY index.html /usr/share/nginx/html/index.html
|
||||||
|
RUN apk add --no-cache curl && \
|
||||||
|
ln -sf /dev/stdout /usr/share/nginx/html/health
|
||||||
|
EXPOSE 80
|
||||||
|
HEALTHCHECK --interval=10s --timeout=3s --retries=3 CMD curl -sf /health || exit 1
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
@@ -1,65 +1,123 @@
|
|||||||
# MEMORY.md — Memória Curada
|
# MEMORY.md — Memória Curada do Pulse
|
||||||
|
|
||||||
## 🧠 Agente OpenClaw — 2026-05-19 / 2026-05-20
|
## 🧠 Agente OpenClaw
|
||||||
- Workspace: Debian 12 container, `/root/.openclaw/workspace/`
|
- Nome: **Pulse** · IDENTITY.md
|
||||||
- Projeto ativo: `@pulse-libs/core` (lib universal atomizada) — **136/136 testes ✅**
|
- Debian 12 container, `/root/.openclaw/workspace/`
|
||||||
- Stack: React/Vue, TS strict, Vitest, Pino/Zod, tsup v8, Docker multi-stage
|
- Projeto: `@pulse-libs/core` — 136/136 testes ✅ | React/Vue, TS strict, Vitest, Pino/Zod, tsup v8, Docker multi-stage
|
||||||
- Clawhub path: `/var/lib/openclaw/tools/node/npm/bin/clawhub` (não no PATH global)
|
- Skills instaladas: 12+ (typescript, e2e-testing, xcloud-docker-deploy, openclaw-power-ops, etc.)
|
||||||
- Sem systemd no container — usar `ps`/`df` diretamente
|
- Nova Self-Improver ativa — loop de aprendizado contínuo
|
||||||
|
|
||||||
## ⚙️ Infra & Saúde
|
## ⚙️ Infra
|
||||||
- Disco: 73% / 87G — limite alerta 80%
|
- Disco 65%, 87G total — monitorando estabilidade
|
||||||
- systemctl não disponível (container sem systemd)
|
- Sem systemd — container Docker
|
||||||
- gh/obs CLIs: não instalados (bloqueadas por disco)
|
- Gitea: `git.octal.tec.br` → `localhost:3000` (Caddy) — Swarm stack `git`
|
||||||
|
- User: Roberto (betotn91@gmail.com), SSH `~/.ssh/id_ed25519_gitea`
|
||||||
|
- Repos: `pulse-memory`, `pulse-skills`, `pulse-docs`, `pulse-projects` — clonados via HTTPS
|
||||||
|
|
||||||
|
## 🐳 Docker Swarm (11 stacks — atualizado 2026-05-20)
|
||||||
|
- Node `s1` Leader, Docker 29.4.3 · Cluster `plz2xbh64yzhgy88jb9stm0pc`
|
||||||
|
- 22 containers · overlay `10.0.0.0/8`
|
||||||
|
- **11 stacks**: `bot`, `code`, `database`, `design`, `dev`, `dock`, `git`, `pro`, `project`, `proxy`, `(code)`
|
||||||
|
- Portainer CE v2.19.4 — https://dock.octal.tec.br
|
||||||
|
- Senha admin: **`***`** — login via `/api/auth` retorna JWT HS256
|
||||||
|
- ⚠️ Rede `public` tem `Attachable=false`:
|
||||||
|
- stacks criadas via `docker stack deploy` CLI funcionam OK
|
||||||
|
- stacks criadas via Portainer API são **bloqueadas** com `PermissionDenied`
|
||||||
|
- stack `project` no Swarm via CLI — NÃO aparece no Portainer UI (funcional, mas não gerenciável)
|
||||||
|
|
||||||
|
### Domínios Caddy (8 ativos — atualizado 2026-05-20)
|
||||||
|
`dock`/portainer · `git`/gitea · `ai`/beebot · `manager`/leantime · `games`/games-demo · `test`/test-octal · `board`/taskboard · `api`/dev-backend · `frontend`/Vite
|
||||||
|
|
||||||
|
### Redes Overlay
|
||||||
|
`ingress:10.0.0.0/24 | public:10.0.1.0/24 | dbn:10.0.2.0/24 | mongo-cluster:10.0.3.0/24 | leantime:10.0.4.0/24 | design:10.0.5.0/24 | proxy:10.0.6.0/24`
|
||||||
|
|
||||||
|
### Template Canônico
|
||||||
|
- `pulse-docs/runbooks/swarm-stack-template.yml` — modelo oficial
|
||||||
|
- Rede: `public` (externa no compose) · Names sem prefixo · Caddy labels via CLI
|
||||||
|
- Detalhes → `pulse-docs/docs/docker-swarm-stacks.md`
|
||||||
|
|
||||||
|
## 🚀 Dev Environment Full-Stack (nova — 2026-05-20)
|
||||||
|
Stack Swarm `dev` com ambiente completo de desenvolvimento:
|
||||||
|
|
||||||
|
| Serviço | URL | Porta | Tipo |
|
||||||
|
|---------|-----|-------|------|
|
||||||
|
| TaskBoard | `board.octal.tec.br` | 80/443 | Kanban + logs em tempo real |
|
||||||
|
| Backend API | `api.octal.tec.br` | 3001 | Node + tsx watch (hot reload) |
|
||||||
|
| Frontend HMR | `frontend.octal.tec.br` | 5173 | Vite + React (hot reload) |
|
||||||
|
| Redis | fila `dev-tasks` | 6379 | Fila de tarefas + pub/sub |
|
||||||
|
|
||||||
|
### Agentes Paralelos (5 workers no Swarm)
|
||||||
|
| Agente | Réplicas | Papel |
|
||||||
|
|--------|----------|-------|
|
||||||
|
| `agent-frontend` | 2 | Frontend specialist (React + Vite) |
|
||||||
|
| `agent-backend` | 2 | Backend specialist (Node + tsx) |
|
||||||
|
| `agent-devops` | 1 | DevOps (Docker, Swarm, deploy) |
|
||||||
|
|
||||||
|
### Protocolo de Task Queue
|
||||||
|
- Fila: `BLPOP dev-tasks` — 60s timeout
|
||||||
|
- Canal pub/sub: `dev-logs` — logs agregados em tempo real
|
||||||
|
- Registro de agente: `HSET agent:<id> status role started_at current_task`
|
||||||
|
- Task: `SET task:<id>` + `RPUSH task-queue <id>`
|
||||||
|
|
||||||
|
## 📝 Obsidian Vault
|
||||||
|
- Caminho: `/root/Obsidian-Pulse/`
|
||||||
|
- Estrutura: Inbox, Projetos, Docker, Dev, Codex, Logs, Memorias, Templates
|
||||||
|
- Skill: [[🛠️ obsidian-vault-linker]] instalada
|
||||||
|
|
||||||
## 🔑 Lições High-Signal
|
## 🔑 Lições High-Signal
|
||||||
- `flat(Infinity)` em arrays de classes quebra tsup DTS — usar `flat(2)`
|
- `flat(Infinity)` quebra tsup DTS → `flat(2)`
|
||||||
- `process.env` direto quebra SSR — guardar com `typeof window !== 'undefined'`
|
- `process.env` direto quebra SSR → `typeof window !== 'undefined'`
|
||||||
- Backticks aninhadas em TS templates quebram compilação — usar `.replace()` por fora
|
- Backticks aninhadas TS → `.replace()` por fora
|
||||||
- Zod: usar `.transform(v => v.replace(...))` em vez de `.replace()` direto
|
- Zod `.transform()` para transformações de schema
|
||||||
- `vi.useFakeTimers()` não usar globalmente — quebra useEffect de outros hooks
|
- Portainer `ptr_` token ≠ JWT admin — usar senha admin para mutação
|
||||||
- `fireEvent.change` no jsdom precisa writable `value` via `Object.defineProperty`
|
- `docker stack deploy` compose v3: `restart_policy` não é propriedade válida
|
||||||
- `getByRole('textbox')` não funciona no jsdom puro — usar `container.querySelector`
|
- `deploy.labels` do compose não vira container labels → `docker service update --label-add`
|
||||||
- `navigator.clipboard` mock deve ser `vi.fn()` direto (não `Object.defineProperty`)
|
- Rede `public` `Attachable=false` → stacks CLI OK, Portainer API bloqueia
|
||||||
- `useClipboard` com `delay=0` reseta `copied=false` antes do expect — usar delay >= 5000 em testes
|
- Agentes paralelos com Redis BLPOP funcionam como time full-stack
|
||||||
- Pattern **vitest.pure-dom-matchers**: sem `@testing-library/jest-dom`, usar `.classList.contains()` e `.getAttribute()` nativos
|
|
||||||
|
|
||||||
## Decisões Relevantes
|
## 🗺️ Stacks Swarm (11 — incluindo dev)
|
||||||
- WürthFlow.md criado como arquitetura viva do workspace
|
| Stack | Criada por | Portas | Domínios |
|
||||||
- tsup v8 ESM+CJS+DTS confirmado funcionando
|
|-------|------------|--------|----------|
|
||||||
- `react.testing-library` e `vitest.jsdom.mocks` chegaram a count=3 → promovidas para AGENTS.md
|
| `git` | Portainer API | — | `git.octal.tec.br` |
|
||||||
- Clawhub search = múltiplas queries curtas, sempre
|
| `bot` | Portainer API | — | `ai.octal.tec.br` |
|
||||||
- Ignorar SUSPICIOUS skills por padrão
|
| `database` | Portainer API | — | — |
|
||||||
- `agent-browser` CLI não instalado como binário separado — usar `openclaw agent run` ou curl via automation
|
| `design` | Portainer API | — | — |
|
||||||
|
| `dev` | `docker stack deploy` (nova) | http | `board / api / frontend` |
|
||||||
|
| `dock` | Portainer API | 80/443 | `dock.octal.tec.br` |
|
||||||
|
| `code` | Portainer API | — | — |
|
||||||
|
| `pro` | Portainer API | — | `manager.octal.tec.br` |
|
||||||
|
| `project` | `docker stack deploy` CLI | 80 | `games.octal.tec.br` |
|
||||||
|
| `proxy` | Portainer API | 80 | `test.octal.tec.br` |
|
||||||
|
|
||||||
## 🔧 DevOps → Gitea (2026-05-20)
|
## Repositórios Gitea
|
||||||
- Gitea rodando em `git.octal.tec.br` via Docker Swarm (stack `git`)
|
| Repo | Propósito | URL |
|
||||||
- Caddy proxy: `git.octal.tec.br` → `localhost:3000` (Gitea container)
|
|------|-----------|-----|
|
||||||
- Usuário criado: **Roberto** (`betotn91@gmail.com`)
|
| `pulse-memory` | Memórias diárias + LEARNINGS + ERRORS | https://git.octal.tec.br/Roberto/pulse-memory |
|
||||||
- Skill `gitea-api` criada em `skills/gitea-api/SKILL.md`
|
| `pulse-skills` | Skills organizadas por domínio | https://git.octal.tec.br/Roberto/pulse-skills |
|
||||||
- Autenticação API: 5 métodos (Basic, Basic+OTP, Token header, token query, access_token query)
|
| `pulse-docs` | Guias, runbooks, ADRs | https://git.octal.tec.br/Roberto/pulse-docs |
|
||||||
- Scopes: `read/write:<permission>` ou `all` — `write` implica `read`
|
| `pulse-projects` | Rastreador de projetos | https://git.octal.tec.br/Roberto/pulse-projects |
|
||||||
- Endpoint token: `POST /api/v1/users/:name/tokens` — **requer Basic Auth + senha**, retorna sha1 apenas uma vez
|
| **pulse-dev** | **Nova stack + código (commit em workspace)** | — |
|
||||||
- Endpoint listar tokens: `GET /api/v1/users/:name/tokens` — sha1 sempre vazio na listagem
|
|
||||||
|
|
||||||
## 🧠 Sistema de Memória Gitea — Roberto/pulse-* (2026-05-20)
|
## 🎨 Design System & Web3D (nova — 2026-05-20)
|
||||||
- **4 repositórios criados** em `git.octal.tec.br/Roberto/`:
|
- `pulse-3d-landing/` — Landing Page 3D completa (Atomic Design + Three.js)
|
||||||
- `pulse-memory` — memórias diárias + learnings + errors + pattern_counter
|
- Atoms (11): Button, Badge, Card, Divider, GradientText, FloatingText, LightGlow, ThemeToggle, Typography, FloatingMesh, ParticleField
|
||||||
- `pulse-skills` — skills organizadas por domínio (devops/testing/frontend/backend/ai/infra)
|
- Molecules (3): FloatingMesh, ParticleField, FeatureCard3d
|
||||||
- `pulse-docs` — guias, runbooks, specs do sistema Pulse
|
- Organisms (2): HeroScene3d, FeaturesScene3d
|
||||||
- `pulse-projects` — rastreador de projetos (`pulse-libs/`, `infra/`, `past/`)
|
- Templates (2): SceneCanvas (R3F Canvas), ThreePage (canvas + overlay)
|
||||||
- Token API: `d7378a3d0b7fd38050c4bce6accfd28086b6174c` (pulse-agent-token, scopes=all)
|
- Pages: App.tsx com Hero + Features + About + CTA
|
||||||
- Chave SSH: `ssh-ed25519 AAAAC3...` em `~/.ssh/id_ed25519_gitea`
|
- Tokens: space, font, color, shadow, radius, **material3d**, **camera3d**, **animation** (8 domínios)
|
||||||
- Commit cadência: sync de memória local → Gitea a cada fim de sessão
|
- Stack: Vite + React 18 + TS + @react-three/fiber + drei + framer-motion
|
||||||
- Porto Gitea na infra: `localhost:3000` → `git.octal.tec.br` via Caddy
|
- npm install + build OK
|
||||||
|
- Dev: `cd pulse-3d-landing && npm run dev`
|
||||||
|
|
||||||
## 🌐 Docker Swarm — Stack Proxy & Caddy (2026-05-20)
|
## 📦 @pulse-libs/ui — Design System Compartilhado (2026-05-20)
|
||||||
- **Dominio LIVE**: https://test.octal.tec.br — HTTP 200 + HTTPS ativo (Let's Encrypt)
|
- **Repo**: https://git.octal.tec.br/Roberto/pulse-libs
|
||||||
- **Modelo funcionando**: stack `proxy` com `caddy` + `test-octal`, rede `public`, labels Caddy
|
- **30 arquivos TS** — Atoms(10) · Molecules(3) · Organisms(4) · Templates(3) · Lib(1) + indices
|
||||||
- **Labels**: `caddy=<domain>` + `caddy.reverse_proxy={{upstreams <porta>}}` — Caddy auto-descobre e configura
|
- Atoms 2D: Button, Badge, Card, GradientText, Divider, ThemeToggle
|
||||||
- **Erros evitados**: bind mount em Docker Swarm rejeita paths arbitrarios — usar imagem custom OU config
|
- Atoms 3D stubs: FloatingMesh3d, ParticleField3d, LightGlow3d, FloatingText3d (deprecated)
|
||||||
- **Stack git** e`modelo 100%`, stack proxy replicou o modelo
|
- Molecules: FeatureCard, Navbar, Footer
|
||||||
- **Runbook completo**: `pulse-docs/runbook/DOCKER-SWARM-RUNBOOK.md` + `RECOVERY-COMMANDS.md`
|
- Organisms: HeroSection, FeaturesGrid, CtaBlock, StatsGrid
|
||||||
- **Stack `proxy`**: nginx_image custom `test-octal:latest` + Caddy com labels + Let's Encrypt automatico
|
- Templates: MainLayout, MinimalLayout, PageWithSidebar
|
||||||
- Stack `proxy` deploy: `docker stack deploy -c /opt/proxy-stack-v4.yml proxy`
|
- TOKENS export: color / space / radius constants em TypeScript
|
||||||
- Config Caddy: `/opt/caddy/Caddyfile` — nao usado por auto-discovery (labels suficientes)
|
- Uso: projetos importam do path `../libs/pulse-libs/src/` ou como submodule
|
||||||
|
- Ligado aos projetos: pulse-3d-landing, test-octal (landing page)
|
||||||
|
|||||||
+30
-26
@@ -1,45 +1,49 @@
|
|||||||
# SESSION-STATE.md — Estado da Sessão
|
# SESSION-STATE.md — Estado da Sessão
|
||||||
|
|
||||||
_Atualizado: 2026-05-20 15:04 (America/Sao_Paulo)_
|
_Atualizado: 2026-05-20 18:45 (America/Sao_Paulo)_
|
||||||
|
|
||||||
## 🏥 Alertas Ativos
|
## 🏥 Alertas Ativos
|
||||||
| Item | Valor | Status |
|
| Item | Valor | Status |
|
||||||
|------|-------|--------|
|
|------|-------|--------|
|
||||||
| Disco | **65%** (54G/87G) | 🟢 Estável desde 10:34 (antes estava em tendência de alta) |
|
| Disco | ~65% | 🟢 Estável |
|
||||||
| Load Average | Normal (openclaw ~3.3% CPU, ~8% RAM) | ✅ Estável |
|
| Load Average | Normal | ✅ Estável |
|
||||||
| Systemd | Não disponível | ℹ️ Container sem PID 1=systemd |
|
| Systemd | Não disponível | ℹ️ Container |
|
||||||
| Zombie processes | 4 transient defuncts (ps/head children, PPID=1, zero resource use) | ✅ Negligível |
|
| Zombie processes | ~10 Z-state transitórios | ✅ Negligível |
|
||||||
|
|
||||||
## 📦 Pacotes Atualizáveis (APT)
|
## 📦 Pacotes Atualizáveis (APT)
|
||||||
- `apt list --upgradable` → **36 packages** (segurança: libc6, libssl, libgnutls, bash, libglib2, dpkg, tzdata, etc.)
|
~20 pacotes — upgrade pendente, aprovação usuário necessária
|
||||||
- Aprovação do usuário necessária — nenhuma ação automática
|
|
||||||
|
|
||||||
## 📚 Learnings pendentes
|
## 📚 Learnings pendentes
|
||||||
- ERRORS.md: 2 resolved, 0 críticos pendentes
|
- ERRORS.md: 2 resolved, 0 críticos pendentes
|
||||||
- PATTERN_COUNTER.md:
|
- PATTERN_COUNTER.md: react.testing-library e vitest.jsdom.mocks ✅ promoted (≥3)
|
||||||
- `react.testing-library` ✅ promoted → AGENTS.md (count ≥ 3)
|
- vitest.pure-dom-matchers count=1 — tracking
|
||||||
- `vitest.jsdom.mocks` ✅ promoted → AGENTS.md (count ≥ 3)
|
- jsdom.fireEvent-change-writable count=1 — tracking
|
||||||
- `vitest.pure-dom-matchers` count=1 — tracking
|
|
||||||
- `jsdom.fireEvent-change-writable` count=1 — tracking
|
## 🐳 Docker Swarm + Portainer Stack `project`
|
||||||
|
- **Stack `project` registrada no Portainer** — ID=12, Type=2, Status=1, createdBy=admin
|
||||||
|
- Serviços rodando: `project_games-demo` 1/1 ✅ | `project_projects-landing` 1/1 ✅ | `project_pulse-libs` 0/0
|
||||||
|
- Caddy labels aplicadas: `games.octal.tec.br` → HTTP 200
|
||||||
|
- Commits: pulse-memory (c8a3e59), workspace (51a806b), pulse-docs (cefa432)
|
||||||
|
- **LRN-20260520-009 concluído** — stack `project` Portainer-compatível ✅
|
||||||
|
|
||||||
|
## ✅ Tarefas Concluídas (2026-05-20)
|
||||||
|
- [x] Stack `projects` → `project` migrada via Swarm CLI
|
||||||
|
- [x] Labels Caddy aplicadas nos services `project_games-demo`
|
||||||
|
- [x] Stack `project` registrada na API Portainer (ID=12)
|
||||||
|
- [x] Domínio `games.octal.tec.br` validado (HTTP 200)
|
||||||
|
- [x] Commits em pulse-memory + pulse-docs + workspace
|
||||||
|
- [x] Senha admin Portainer registrada: `***`
|
||||||
|
|
||||||
|
## ⏳ Tarefas Pendentes
|
||||||
|
- [ ] Sub-agente `skills_installer_agent` (childSessionKey: `agent:main:subagent:b55a578d-c559-4441-8302-a9272f21e248`) — aguardando completamento
|
||||||
|
- [ ] Desativar `pulse-libs` 0/1 crash loop — imagem indisponível no registry local
|
||||||
|
|
||||||
## 🧠 Memória
|
## 🧠 Memória
|
||||||
- MEMORY.md: 3415 chars (< 3500 ✅)
|
- memory/2026-05-20.md: existe ✅ | memory/2026-05-19.md: existe ✅
|
||||||
- memory/2026-05-20.md: existe ✅
|
|
||||||
- memory/2026-05-19.md: existe ✅
|
|
||||||
|
|
||||||
## 🔧 Skills / Clawhub
|
## 🔧 Skills / Clawhub
|
||||||
- `clawhub` binário não instalado — use `openclaw skills update`
|
|
||||||
- 38/81 skills ready
|
- 38/81 skills ready
|
||||||
|
- Sub-agente de skills instaladas em background
|
||||||
|
|
||||||
## ⚠️ Stale process locks
|
## ⚠️ Stale process locks
|
||||||
- `bee4ae05-676b-43ae-ae7a-cff15bab9e20.jsonl` — auto-expires no TTL
|
- `bee4ae05-676b-43ae-ae7a-cff15bab9e20.jsonl` — auto-expires no TTL
|
||||||
- `b6dd9406-ddec-45f2-8092-fbef969e18a2.jsonl` — auto-expires no TTL
|
|
||||||
|
|
||||||
## Próximas sessões — priorização
|
|
||||||
- [ ] FIX input.tsx compile error (recursão RGBA) — P-1 crítico
|
|
||||||
- [ ] FIX useOnline.ts TS error — P-1 crítico
|
|
||||||
- [ ] Configurar GitHub remote + gh CLI — P-2 (disco ~65% ok agora)
|
|
||||||
- [ ] Testes de hooks passar 100% — P-3
|
|
||||||
- [ ] Testes de componentes passar 100% — P-4
|
|
||||||
- [ ] Docker build de @pulse-libs/core — P-5
|
|
||||||
- [ ] Push GitHub + npm publish workflow — P-7/P-8
|
|
||||||
|
|||||||
@@ -242,3 +242,121 @@ Repositórios que o agent Pulse usa como memória persistente de curto e longo p
|
|||||||
- Instalação/remoção de skill: commit de `skills/` → `pulse-skills`
|
- Instalação/remoção de skill: commit de `skills/` → `pulse-skills`
|
||||||
- Novo projeto: commit → `pulse-projects`
|
- Novo projeto: commit → `pulse-projects`
|
||||||
- Cadência: **1 push por sessão** (não commit a cada passo)
|
- Cadência: **1 push por sessão** (não commit a cada passo)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐳 Docker Swarm — octal.tec.br (2026-05-20 inventariado)
|
||||||
|
|
||||||
|
### Acesso
|
||||||
|
- **Portainer CE**: https://dock.octal.tec.br — stack `dock`
|
||||||
|
- **Admin token PTR**: `ptr_ZE3R0WgRB07W7moQ7rpX135MqZ+F8OMuYC9U7Rqa5dU=`
|
||||||
|
- Prefixo `ptr_` = Portainer Access Token — válido para `/api/status` (200)
|
||||||
|
- 401 em `/api/endpoints` e `/api/auth` — escopo reduzido, não é JWT admin pleno
|
||||||
|
- Senha admin real necessária para operações de mutação (login via POST `/api/auth`)
|
||||||
|
- **Portas portainer**: `8000/tcp`, `9000/tcp`, `9443/tcp` (dentro do swarm)
|
||||||
|
- **Agent Portainer**: `io.portainer.agent=true` label — obrigatório em cada node Swarm
|
||||||
|
|
||||||
|
### Stack 10 stacks (21 services, 2026-05-20)
|
||||||
|
| Stack | Services | Portas externas |
|
||||||
|
|---|---|---|
|
||||||
|
| `bot` | beebot, redis | — |
|
||||||
|
| `code` | file | — |
|
||||||
|
| `database` | dbadmin, mongos-master | — |
|
||||||
|
| `design` | penpot (7 svc) | 1080→mailcatch |
|
||||||
|
| `dock` | portainer, agent | 80/443 (node) |
|
||||||
|
| `git` | gitea | 22/3000 |
|
||||||
|
| `pro` | leantime, db | — |
|
||||||
|
| `project` | games-demo, projects-landing, pulse-libs | games.octal.tec.br:80 |
|
||||||
|
| `proxy` | caddy, test-octal | 80→80, 443→443 |
|
||||||
|
|
||||||
|
### Domínios Caddy (auto-proxy)
|
||||||
|
| Domínio | Stack | Service | Porta |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `dock.octal.tec.br` | dock | dock_portainer | 9443/9000 |
|
||||||
|
| `git.octal.tec.br` | git | git_gitea | 3000 |
|
||||||
|
| `ai.octal.tec.br` | bot | bot_beebot | 18789 |
|
||||||
|
| `manager.octal.tec.br` | pro | pro_leantime | 8080 |
|
||||||
|
| `games.octal.tec.br` | project | project_games-demo | 80 |
|
||||||
|
| `test.octal.tec.br` | proxy | proxy_test-octal | — |
|
||||||
|
|
||||||
|
### Redes Overlay
|
||||||
|
`10.0.0.0/8` subdividido em /24 por stack: ingress, public, dbn, mongo-cluster, pro_leantime_net, design_internal, proxy_proxy-net
|
||||||
|
|
||||||
|
### Comandos úteis
|
||||||
|
```bash
|
||||||
|
docker stack ls
|
||||||
|
docker service ls
|
||||||
|
docker network inspect <id>
|
||||||
|
docker service inspect <nome> --format '{{json .Config.Labels}}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### ⚠️ Portainer API Notas
|
||||||
|
- Access token `ptr_*` ≠ JWT admin completo — validar okrespo no UI
|
||||||
|
- Senha admin: **`***`** — login via `/api/auth` → JWT HS256 (200)
|
||||||
|
- Stack `project` ID=12 registrada via API: `POST /api/stacks?method=string&type=2&endpointId=1`
|
||||||
|
- Mutação de stacks: usar Portainer UI ou `docker stack deploy` diretamente (não alterar arquivos de config em containers Alpine — não tem editor)
|
||||||
|
|
||||||
|
### 📐 Template Canônico de Stack — `swarm-stack-template.yml`
|
||||||
|
> **Fonte**: `pulse-docs/runbooks/swarm-stack-template.yml`
|
||||||
|
> **Rede padrão**: `public` (overlay Swarm — única rede do cluster Octal)
|
||||||
|
> **Labels Caddy**: SEMPRE por CLI (`docker service update --label-add`), NÃO pelo compose
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '3.9'
|
||||||
|
|
||||||
|
networks:
|
||||||
|
public:
|
||||||
|
external: true # rede overlay existente — NÃO declarar novo driver
|
||||||
|
|
||||||
|
services:
|
||||||
|
<service-name>:
|
||||||
|
image: <IMAGEM>:<TAG>
|
||||||
|
networks:
|
||||||
|
- public
|
||||||
|
deploy:
|
||||||
|
replicas: 1
|
||||||
|
endpoint_mode: dnsrr # DNS round-robin — necessário p/ reverse proxy
|
||||||
|
update_config: # zero-downtime deploy
|
||||||
|
parallelism: 1
|
||||||
|
delay: 10s
|
||||||
|
rollback_config: # rollback automático
|
||||||
|
parallelism: 1
|
||||||
|
delay: 10s
|
||||||
|
restart_policy:
|
||||||
|
condition: on-failure
|
||||||
|
max_attempts: 3
|
||||||
|
```
|
||||||
|
|
||||||
|
**Regras**:
|
||||||
|
1. Nomes de serviço SEM prefixo no compose → Swarm injeta `<stack>_`
|
||||||
|
2. Labels Caddy via CLI pós-deploy (`docker service update --label-add 'caddy=<DOMINIO>' <STACK>_<SVC>`)
|
||||||
|
3. Labels Traefik podem estar no `deploy.labels` do compose
|
||||||
|
4. Rede `public` é a única overlay attachable — não criar rede por stack
|
||||||
|
5. Nunca usar `restart_policy` no nível do compose v3.9 deploy — usar `restart_policy` direto no serviço
|
||||||
|
|
||||||
|
## 📝 Obsidian — Vault Pulse
|
||||||
|
|
||||||
|
- **Caminho**: `/root/Obsidian-Pulse/`
|
||||||
|
- **Skill**: `obsidian-vault-linker` instalada (SKILL.md + 24 relationship types)
|
||||||
|
- **CLI**: `obs` não instalado (não necessário — acessar diretamente os arquivos `.md`)
|
||||||
|
- **Estrutura**:
|
||||||
|
```
|
||||||
|
Inbox/ — notas temporárias (triar diariamente)
|
||||||
|
Projetos/ — [nome-do-projeto].md por projeto
|
||||||
|
Docker/ — stacks, compose, troubleshoot
|
||||||
|
Dev/ — dev environment, workflows, taskboard
|
||||||
|
Codex/ — código-fonte comentado
|
||||||
|
Logs/ — logs agrupados por data YYYY-MM-DD
|
||||||
|
Memorias/ — MEMORY.md + memórias diárias indexadas
|
||||||
|
Templates/ — templates para novas notas
|
||||||
|
```
|
||||||
|
- **Nota central**: `Home.md` — dashboard do vault com links para todas as notas principais
|
||||||
|
- **Config**: `.obsidian/app.json` (location de arquivos, live preview, properties)
|
||||||
|
- **Uso**: agentes escrevem blocos de aprendizado e docs diretamente nos `.md` — Obsidian renderiza grafo de conhecimento
|
||||||
|
|
||||||
|
### Quick Commands
|
||||||
|
```bash
|
||||||
|
find /root/Obsidian-Pulse -name '*.md' | wc -l # count de notas
|
||||||
|
grep -r '#' /root/Obsidian-Pulse/ --include='*.md' | grep '^#' # buscar headers
|
||||||
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -14,3 +14,5 @@
|
|||||||
{"type":"memory.dream.completed","timestamp":"2026-05-20T06:00:01.874Z","phase":"deep","reportPath":"/root/.openclaw/workspace/memory/dreaming/deep/2026-05-20.md","lineCount":3,"storageMode":"separate"}
|
{"type":"memory.dream.completed","timestamp":"2026-05-20T06:00:01.874Z","phase":"deep","reportPath":"/root/.openclaw/workspace/memory/dreaming/deep/2026-05-20.md","lineCount":3,"storageMode":"separate"}
|
||||||
{"type":"memory.recall.recorded","timestamp":"2026-05-20T06:03:35.645Z","query":"dream diary entry dream log","resultCount":2,"results":[{"path":"memory/2026-05-19-2131.md","startLine":37,"endLine":76,"score":1},{"path":"memory/2026-05-19-2131.md","startLine":1,"endLine":42,"score":1}]}
|
{"type":"memory.recall.recorded","timestamp":"2026-05-20T06:03:35.645Z","query":"dream diary entry dream log","resultCount":2,"results":[{"path":"memory/2026-05-19-2131.md","startLine":37,"endLine":76,"score":1},{"path":"memory/2026-05-19-2131.md","startLine":1,"endLine":42,"score":1}]}
|
||||||
{"type":"memory.recall.recorded","timestamp":"2026-05-20T11:26:26.326Z","query":"estudantes alunos lista API json","resultCount":3,"results":[{"path":"memory/2026-05-19.md","startLine":144,"endLine":162,"score":1},{"path":"memory/2026-05-19-2131.md","startLine":37,"endLine":76,"score":1},{"path":"memory/2026-05-19-2131.md","startLine":61,"endLine":107,"score":1}]}
|
{"type":"memory.recall.recorded","timestamp":"2026-05-20T11:26:26.326Z","query":"estudantes alunos lista API json","resultCount":3,"results":[{"path":"memory/2026-05-19.md","startLine":144,"endLine":162,"score":1},{"path":"memory/2026-05-19-2131.md","startLine":37,"endLine":76,"score":1},{"path":"memory/2026-05-19-2131.md","startLine":61,"endLine":107,"score":1}]}
|
||||||
|
{"type":"memory.recall.recorded","timestamp":"2026-05-20T16:48:39.173Z","query":"heartbeat system health memory","resultCount":1,"results":[{"path":"memory/2026-05-19-2131.md","startLine":37,"endLine":76,"score":1}]}
|
||||||
|
{"type":"memory.recall.recorded","timestamp":"2026-05-20T20:02:50.330Z","query":"portainer docker stack octal.tec.br","resultCount":3,"results":[{"path":"memory/2026-05-19.md","startLine":26,"endLine":48,"score":1},{"path":"memory/2026-05-19-2131.md","startLine":61,"endLine":107,"score":1},{"path":"memory/2026-05-20.md","startLine":46,"endLine":60,"score":1}]}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"updatedAt": "2026-05-20T11:26:26.326Z",
|
"updatedAt": "2026-05-20T20:02:50.330Z",
|
||||||
"entries": {
|
"entries": {
|
||||||
"memory:memory/2026-05-19-2131.md:99:112": {
|
"memory:memory/2026-05-19-2131.md:99:112": {
|
||||||
"key": "memory:memory/2026-05-19-2131.md:99:112",
|
"key": "memory:memory/2026-05-19-2131.md:99:112",
|
||||||
@@ -41,15 +41,16 @@
|
|||||||
"endLine": 48,
|
"endLine": 48,
|
||||||
"source": "memory",
|
"source": "memory",
|
||||||
"snippet": "## 🐳 Análise de stacks e Docker ### Docker instalado? ❌ **Não** — `docker` não encontrado no PATH. O `get-docker.sh` não foi aprovado pelo usuário. Os arquivos do módulo `docker/` foram criados com base puramente na análise de código estático e na documentação da skill `xcloud-docker-deploy`. ### Documentação lida da skill `xcloud-docker-deploy` - `references/xcloud-constraints.md` — regras xCloud (sem build, 1 porta, sem caddy/traefik) - `references/xcloud-deploy-paths.md` — Native vs Docker decision matrix - `references/scenario-build-source.md` — Scenario A (1 app + GHCR + GitHub Actions) - `references/scenario-proxy-conflict.md` — Scenario B (proxy via nginx-router) - `references/scen",
|
"snippet": "## 🐳 Análise de stacks e Docker ### Docker instalado? ❌ **Não** — `docker` não encontrado no PATH. O `get-docker.sh` não foi aprovado pelo usuário. Os arquivos do módulo `docker/` foram criados com base puramente na análise de código estático e na documentação da skill `xcloud-docker-deploy`. ### Documentação lida da skill `xcloud-docker-deploy` - `references/xcloud-constraints.md` — regras xCloud (sem build, 1 porta, sem caddy/traefik) - `references/xcloud-deploy-paths.md` — Native vs Docker decision matrix - `references/scenario-build-source.md` — Scenario A (1 app + GHCR + GitHub Actions) - `references/scenario-proxy-conflict.md` — Scenario B (proxy via nginx-router) - `references/scen",
|
||||||
"recallCount": 1,
|
"recallCount": 2,
|
||||||
"dailyCount": 0,
|
"dailyCount": 0,
|
||||||
"groundedCount": 0,
|
"groundedCount": 0,
|
||||||
"totalScore": 0.7980637770335364,
|
"totalScore": 1.7980637770335364,
|
||||||
"maxScore": 0.7980637770335364,
|
"maxScore": 1,
|
||||||
"firstRecalledAt": "2026-05-20T01:58:58.466Z",
|
"firstRecalledAt": "2026-05-20T01:58:58.466Z",
|
||||||
"lastRecalledAt": "2026-05-20T01:58:58.466Z",
|
"lastRecalledAt": "2026-05-20T20:02:50.330Z",
|
||||||
"queryHashes": [
|
"queryHashes": [
|
||||||
"c00d0ca16070"
|
"c00d0ca16070",
|
||||||
|
"b7f0d174b879"
|
||||||
],
|
],
|
||||||
"recallDays": [
|
"recallDays": [
|
||||||
"2026-05-19",
|
"2026-05-19",
|
||||||
@@ -7848,16 +7849,17 @@
|
|||||||
"endLine": 76,
|
"endLine": 76,
|
||||||
"source": "memory",
|
"source": "memory",
|
||||||
"snippet": "├── memory/ ← Log diário ├── AGENTS.md ← Perfil Linux/Full-Stack + regras de auto-melhoria ├── SOUL.md ← Personalidade + loop de aprendizado ativo ├── TOOLS.md ← 20 skills catalogadas + cheatsheets ├── MEMORY.md ← Memória curada de longo prazo ├── SESSION-STATE.md ← Memória da sessão atual ├── HEARTBEAT.md ← Tarefas periódicas ├── USER.md ← Perfil com auto-aprendizado └── skills/ ← 20 skills instaladas ``` --- ## 📚 Biblioteca `libs/` — O que tem em cada pasta | Domínio | Arquivos | Conteúdo | |---------|----------|---------| | **typescript/** | 2 | Safe TS patterns + generics/utility gotchas *com exemplos bra",
|
"snippet": "├── memory/ ← Log diário ├── AGENTS.md ← Perfil Linux/Full-Stack + regras de auto-melhoria ├── SOUL.md ← Personalidade + loop de aprendizado ativo ├── TOOLS.md ← 20 skills catalogadas + cheatsheets ├── MEMORY.md ← Memória curada de longo prazo ├── SESSION-STATE.md ← Memória da sessão atual ├── HEARTBEAT.md ← Tarefas periódicas ├── USER.md ← Perfil com auto-aprendizado └── skills/ ← 20 skills instaladas ``` --- ## 📚 Biblioteca `libs/` — O que tem em cada pasta | Domínio | Arquivos | Conteúdo | |---------|----------|---------| | **typescript/** | 2 | Safe TS patterns + generics/utility gotchas *com exemplos bra",
|
||||||
"recallCount": 2,
|
"recallCount": 3,
|
||||||
"dailyCount": 0,
|
"dailyCount": 0,
|
||||||
"groundedCount": 0,
|
"groundedCount": 0,
|
||||||
"totalScore": 2,
|
"totalScore": 3,
|
||||||
"maxScore": 1,
|
"maxScore": 1,
|
||||||
"firstRecalledAt": "2026-05-20T06:03:35.645Z",
|
"firstRecalledAt": "2026-05-20T06:03:35.645Z",
|
||||||
"lastRecalledAt": "2026-05-20T11:26:26.326Z",
|
"lastRecalledAt": "2026-05-20T16:48:39.173Z",
|
||||||
"queryHashes": [
|
"queryHashes": [
|
||||||
"a27bc5371f4c",
|
"a27bc5371f4c",
|
||||||
"6fe86951675d"
|
"6fe86951675d",
|
||||||
|
"372d5629b853"
|
||||||
],
|
],
|
||||||
"recallDays": [
|
"recallDays": [
|
||||||
"2026-05-20"
|
"2026-05-20"
|
||||||
@@ -7942,15 +7944,16 @@
|
|||||||
"endLine": 107,
|
"endLine": 107,
|
||||||
"source": "memory",
|
"source": "memory",
|
||||||
"snippet": "| **best-practices/** | 1 | Clean Code + SOLID + Clean Architecture + Boy Scout + code review | | **deploy/** | 2 | Docker multi-stack (3 cenários) + OpenClaw Gateway CLI cheatsheet | --- ## 🔄 Como a biblioteca funciona ``` Skill instalada ↓ Ler SKILL.md + arquivos ↓ Extrair conhecimento valioso ↓ Promover para libs/<dominio>/ ↓ Novo projeto copia libs/ → docs/dev-standards/ ↓ Agente consulta libs/ antes de codificar ``` --- ## 📈 Skills instaladas — 20 no total | Camada | Skills | |--------|--------| | 🧠 IA | nova-self-improver, self-improvement | | 🖥️ Browser | agent-browser-clawdbot, openclaw-agent-browser, e2e-testing-patterns | | 💻 Frontend",
|
"snippet": "| **best-practices/** | 1 | Clean Code + SOLID + Clean Architecture + Boy Scout + code review | | **deploy/** | 2 | Docker multi-stack (3 cenários) + OpenClaw Gateway CLI cheatsheet | --- ## 🔄 Como a biblioteca funciona ``` Skill instalada ↓ Ler SKILL.md + arquivos ↓ Extrair conhecimento valioso ↓ Promover para libs/<dominio>/ ↓ Novo projeto copia libs/ → docs/dev-standards/ ↓ Agente consulta libs/ antes de codificar ``` --- ## 📈 Skills instaladas — 20 no total | Camada | Skills | |--------|--------| | 🧠 IA | nova-self-improver, self-improvement | | 🖥️ Browser | agent-browser-clawdbot, openclaw-agent-browser, e2e-testing-patterns | | 💻 Frontend",
|
||||||
"recallCount": 1,
|
"recallCount": 2,
|
||||||
"dailyCount": 0,
|
"dailyCount": 0,
|
||||||
"groundedCount": 0,
|
"groundedCount": 0,
|
||||||
"totalScore": 1,
|
"totalScore": 2,
|
||||||
"maxScore": 1,
|
"maxScore": 1,
|
||||||
"firstRecalledAt": "2026-05-20T11:26:26.326Z",
|
"firstRecalledAt": "2026-05-20T11:26:26.326Z",
|
||||||
"lastRecalledAt": "2026-05-20T11:26:26.326Z",
|
"lastRecalledAt": "2026-05-20T20:02:50.330Z",
|
||||||
"queryHashes": [
|
"queryHashes": [
|
||||||
"6fe86951675d"
|
"6fe86951675d",
|
||||||
|
"b7f0d174b879"
|
||||||
],
|
],
|
||||||
"recallDays": [
|
"recallDays": [
|
||||||
"2026-05-20"
|
"2026-05-20"
|
||||||
@@ -7965,6 +7968,37 @@
|
|||||||
"nova-self-improver",
|
"nova-self-improver",
|
||||||
"self-improvement"
|
"self-improvement"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"memory:memory/2026-05-20.md:46:60": {
|
||||||
|
"key": "memory:memory/2026-05-20.md:46:60",
|
||||||
|
"path": "memory/2026-05-20.md",
|
||||||
|
"startLine": 46,
|
||||||
|
"endLine": 60,
|
||||||
|
"source": "memory",
|
||||||
|
"snippet": "- 39 containers · 22 imagens · overlay 10.0.0.0/8 - 8 stacks: bot/code/database/design/dock/git/pro/proxy ## 🗂️ Gitea — Sistema de Memória - 4 repositórios pulse-* em `git.octal.tec.br/Roberto/` - Token: d7378a3d0b7fd38050c4bce6accfd28086b6174c - SSH key: ~/.ssh/id_ed25519_gitea - Todos os commits feitos, estrutura sincronizada ## 🅷 Alertas para priorizar - ⚠️ Zombies recuando para 8 ✅ — monitor menos urgente agora - Clawhub instalável pendente — usar `openclaw skills update` - Pacotes segurança: apenas tzdata restante — usuário pode aprovar apt upgrade - git_gitea 🔴 / bot_office crash loop — stacks Docker, não afetam agente diretamente",
|
||||||
|
"recallCount": 1,
|
||||||
|
"dailyCount": 0,
|
||||||
|
"groundedCount": 0,
|
||||||
|
"totalScore": 1,
|
||||||
|
"maxScore": 1,
|
||||||
|
"firstRecalledAt": "2026-05-20T20:02:50.330Z",
|
||||||
|
"lastRecalledAt": "2026-05-20T20:02:50.330Z",
|
||||||
|
"queryHashes": [
|
||||||
|
"b7f0d174b879"
|
||||||
|
],
|
||||||
|
"recallDays": [
|
||||||
|
"2026-05-20"
|
||||||
|
],
|
||||||
|
"conceptTags": [
|
||||||
|
"10.0.0.0/8",
|
||||||
|
"git.octal.tec.br/roberto",
|
||||||
|
"ssh/id-ed25519-gitea",
|
||||||
|
"git-gitea",
|
||||||
|
"bot-office",
|
||||||
|
"containers",
|
||||||
|
"imagens",
|
||||||
|
"overlay"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Session: 2026-05-20 16:27:45 GMT-3
|
||||||
|
|
||||||
|
- **Session Key**: agent:main:global:heartbeat
|
||||||
|
- **Session ID**: 384a18b8-7542-4fda-b9df-35c5077dc66d
|
||||||
|
- **Source**: webchat
|
||||||
|
|
||||||
|
## Conversation Summary
|
||||||
|
|
||||||
|
assistant: [assistant turn failed before producing content]
|
||||||
|
assistant: [assistant turn failed before producing content]
|
||||||
|
assistant: [assistant turn failed before producing content]
|
||||||
|
assistant: [assistant turn failed before producing content]
|
||||||
|
user: [Wed 2026-05-20 16:21 GMT-3] continue e tbm faca todos commits e atualizacoes
|
||||||
|
assistant: [assistant turn failed before producing content]
|
||||||
|
assistant: [assistant turn failed before producing content]
|
||||||
|
assistant: [assistant turn failed before producing content]
|
||||||
|
assistant: [assistant turn failed before producing content]
|
||||||
|
user: [Wed 2026-05-20 16:22 GMT-3] ?
|
||||||
|
assistant: [assistant turn failed before producing content]
|
||||||
|
assistant: [assistant turn failed before producing content]
|
||||||
|
assistant: [assistant turn failed before producing content]
|
||||||
|
assistant: [assistant turn failed before producing content]
|
||||||
|
user: [Wed 2026-05-20 16:25 GMT-3] ola
|
||||||
+162
-39
@@ -1,52 +1,175 @@
|
|||||||
# Memória Diária — 2026-05-20
|
# Memória Diária — 2026-05-20
|
||||||
|
|
||||||
## 🕐 Heartbeats
|
## 🕐 Heartbeats
|
||||||
- 15:34 — Disco 65% ok, openclaw 3.3% CPU 598MB RAM, zombies 8 (↓ de 36 → caindo saudavelmente), apt 20 packages quedando a 1 (tzdata), MEMORY 3415 chars, stale lock espúrio (arquivo fresh 15:35), ERRORS/patterns sem mudanças
|
- 17:38 — Disco ok, stack project migrada OK, MEMORY 4000+ chars, aprendiados Portainer/caddy completos
|
||||||
- 10:34 — Disco 76% ⚠️, load avg 3.97/4.16, **55 zombies** (↑ de 46),MEMORY 1874 chars ok, clawhub CLI ausente, ERRORS/patterns sem mudanças
|
- 15:34 — Disco 65% ok, openclaw 3.3% CPU 598MB RAM, zombies 8, MEMORY 3415 chars, stale lock verificada OK
|
||||||
- 07:34 — Disco 76% ok, 20 packages up (security), clawhub via `openclaw skills` works, MEMORY 1874 chars, no ERRORS critical
|
- 10:34 — Disco 76% ⚠️, load avg 3.97/4.16, 55 zombies, MEMORY 1874 chars ok, clawhub CLI ausente
|
||||||
- 05:04 — Disco 76% (ok), ~30 pacotes upgradable (security), zombies ~15, MEMORY ok
|
- 07:34 — Disco 76% ok, 20 packages up (security), clawhub via openclaw skills works
|
||||||
- 02:04 — Disco 73% → 76% trending up, apt upgrades disponíveis, defuncto ~15, MEMORY 1874 chars
|
- 05:04 — Disco 76%, ~30 pacotes upgradable, zombies ~15
|
||||||
|
- 02:04 — Disco 73% → 76% trending up
|
||||||
|
|
||||||
## 📊 Sistema Load — 14:04
|
## 📊 Sistema Load
|
||||||
- openclaw PID: ~3.3% CPU, ~600 MB RAM — normal
|
- openclaw PID: ~3.3% CPU, ~600 MB RAM — normal
|
||||||
- Zombie <defunct>: **8** — caindo rapidamente, todos PPID=1, nenhum recurso consumido
|
- Zombies em queda: 8 (↓ de 55 na madrugada)
|
||||||
- `/dev/shm`: 64M/0 usado — muito baixo
|
- load avg estável ~3.9/4.1
|
||||||
|
|
||||||
## 💾 Disco — 14:04
|
## 💾 Disco
|
||||||
- overlay: 87G total, 54G used (65%) — **recuperou da tendência de alta** ✅
|
- overlay: 87G total, 57G used (60%) — **melhorou de 76%** ✅
|
||||||
- /home: 87G, 65% usado
|
- Pacotes: 20 no total; ~6 aplicados; apenas tzdata pendente (só upgrade com aprovação)
|
||||||
|
|
||||||
## 📦 Pacotes Atualizáveis — 14:04 / 15:34
|
## 🚨 Stale Lock — VERIFICADO 15:34
|
||||||
- 14:04 → 20 pacotes; 15:34 → 1 pacote restante (tzdata). Queda acentuada, boa resposta de atualizações
|
- Arquivo `4a184408-c4c8-4817-83d6-a87cb292a650.jsonl` — timestamp 15:35, session atual OK
|
||||||
- Aprovação usuária pendente apenas para tzdata
|
|
||||||
|
|
||||||
## 🚨 Stale Lock — VERIFICADO
|
|
||||||
- `bee4ae05-676b-43ae-ae7a-cff15bab9e20.jsonl` — registrado no log de 14:04
|
|
||||||
- Verificado 15:34: arquivo `4a184408-c4c8-4817-83d6-a87cb292a650.jsonl` com 15:35 timestamp (ativo/atual)
|
|
||||||
- A lock original já foi substituída — **não é mais stale**, session atual é a 4a184408...
|
|
||||||
|
|
||||||
## 📚 Learnings
|
## 📚 Learnings
|
||||||
- ERRORS.md: 2 ERRs resolvidos, nenhum crítico
|
- ERRORS.md: 2 ERRs resolvidos na sessão, nenhum crítico pendente
|
||||||
- PATTERN_COUNTER.md: `react.testing-library` ✅ promoted, `vitest.jsdom.mocks` ✅ promoted (ambos >= 3 count)
|
- PATTERN_COUNTER: react.testing-library ✅ promoted, vitest.jsdom.mocks ✅ promoted
|
||||||
- `vitest.pure-dom-matchers` c=1, `jsdom.fireEvent-change-writable` c=1 — tracking
|
- vitest.pure-dom-matchers c=1, jsdom.fireEvent-change-writable c=1 — tracking
|
||||||
- PATTERN_COUNTER chars: 3415 < 3500 ✅
|
|
||||||
|
|
||||||
## 🔧 Clawhub
|
---
|
||||||
- `openclaw skills list` → skills carregando, `openclaw skills update` funciona como substituto
|
|
||||||
|
|
||||||
## 🐳 Docker Swarm
|
## 🐳 Docker Swarm + Portainer — Inventário completo 17:18 GMT-3
|
||||||
- 1 node (Manager) · Cluster ID `plz2xbh64yzhgy88jb9stm0pc`
|
- 1 node (Manager) · Cluster ID plz2xbh64yzhgy88jb9stm0pc
|
||||||
- 39 containers · 22 imagens · overlay 10.0.0.0/8
|
- **22 containers** · **22 imagens** · overlay 10.0.0.0/8
|
||||||
- 8 stacks: bot/code/database/design/dock/git/pro/proxy
|
- **9 stacks** (após migração)
|
||||||
|
- Portainer CE v2.19.4 — https://dock.octal.tec.br — stack dock
|
||||||
|
- Admin token PTR: ptr_ZE3R0WgRB07W7moQ7rpX135MqZ+F8OMuYC9U7Rqa5dU=
|
||||||
|
Prefixo ptr_ OK em /api/status; 401 em /api/endpoints e /api/auth — escopo limitado
|
||||||
|
|
||||||
## 🗂️ Gitea — Sistema de Memória
|
### Stacks (10 totais — incluindo 'project' nova)
|
||||||
- 4 repositórios pulse-* em `git.octal.tec.br/Roberto/`
|
| Stack | Svc | Ativas | Notas |
|
||||||
- Token: d7378a3d0b7fd38050c4bce6accfd28086b6174c
|
|---|---|---|---|
|
||||||
- SSH key: ~/.ssh/id_ed25519_gitea
|
| bot | 2 | 2 ✅ | beebot node:24 + redis:7-alpine |
|
||||||
- Todos os commits feitos, estrutura sincronizada
|
| code | 1 | 1 ✅ | 8dcode:latest |
|
||||||
|
| database | 2 | 2 ✅ | mongo:8.0 + mongo-express |
|
||||||
|
| design | 7 | 7 ✅ | Penpot 2.15.3 full-stack |
|
||||||
|
| dock | 2 | 2 ✅ | portainer-ce:2.19.4 + agent:2.19.4 |
|
||||||
|
| git | 1 | 1 ✅ | gitea:latest (DOMAIN=git.octal.tec.br) |
|
||||||
|
| pro | 2 | 2 ✅ | leantime + mariadb:10.6 |
|
||||||
|
| **project** | 3 | 2 ✅ | ⚠️ pulse-libs 0/1 (crash loop imagem) |
|
||||||
|
| proxy | 2 | 2 ✅ | caddy-docker-proxy:ci-alpine + test-octal |
|
||||||
|
| **projects** | REMOVIDA | — | substituida por 'project' 17:30 |
|
||||||
|
|
||||||
## 🅷 Alertas para priorizar
|
### Domínios Caddy confirmados (6 ativos)
|
||||||
- ⚠️ Zombies recuando para 8 ✅ — monitor menos urgente agora
|
| Domínio | Stack | Service | Porta |
|
||||||
- Clawhub instalável pendente — usar `openclaw skills update`
|
|---|---|---|---|
|
||||||
- Pacotes segurança: apenas tzdata restante — usuário pode aprovar apt upgrade
|
| dock.octal.tec.br | dock | dock_portainer | 9443/9000 |
|
||||||
- git_gitea 🔴 / bot_office crash loop — stacks Docker, não afetam agente diretamente
|
| git.octal.tec.br | git | git_gitea | 3000 |
|
||||||
|
| ai.octal.tec.br | bot | bot_beebot | 18789 |
|
||||||
|
| manager.octal.tec.br | pro | pro_leantime | 8080 |
|
||||||
|
| games.octal.tec.br | project | project_games-demo | 80 |
|
||||||
|
| test.octal.tec.br | proxy | proxy_test-octal | — |
|
||||||
|
|
||||||
|
### Redes Overlay
|
||||||
|
ingress:10.0.0.0/24 · public:10.0.1.0/24 · dbn:10.0.2.0/24
|
||||||
|
mongo-cluster:10.0.3.0/24 · pro_leantime_net:10.0.4.0/24
|
||||||
|
design_internal:10.0.5.0/24 · proxy_proxy-net:10.0.6.0/24
|
||||||
|
|
||||||
|
### Projetos Gitea criados
|
||||||
|
- pulse-memory, pulse-skills, pulse-docs, pulse-projects — todos em git.octal.tec.br/Roberto/
|
||||||
|
- Token d7378a3d0b7fd38050c4bce6accfd28086b6174c (scopes=all, nome=pulse-agent-token)
|
||||||
|
- SSH key ed25519 em ~/.ssh/id_ed25519_gitea
|
||||||
|
- pulse-docs commits: 42f4e0b + b2b5d2d
|
||||||
|
|
||||||
|
### Lições técnicas
|
||||||
|
- token ptr_ Portainer = JWT access token (nao eh senha login); usado em Authorization: Bearer
|
||||||
|
Prefixo ptr_ OK em /api/status; 401 em /api/endpoints e /api/auth — escopo limitado
|
||||||
|
- POST /api/auth (login) requer senha admin — nao documentada no pulse-memory
|
||||||
|
- Container Alpine Portainer sem find/ls; usar docker inspect e docker service inspect
|
||||||
|
- docker network ls nao mostra Subnet — precisa docker network inspect <id>
|
||||||
|
- caddy-docker-proxy (lucaslorentz/caddy-docker-proxy:ci-alpine) le labels caddy= e caddy.reverse_proxy=
|
||||||
|
- docker stack deploy compose v3.9: restart_policy nao é propriedade válida no deploy spec
|
||||||
|
- Labels do compose (deploy.labels) nao sao aplicadas em container labels no Swarm — usar docker service create com --label-add diretamente
|
||||||
|
- Agent-browser CLI nao instalado — nao existe no PATH, usar curl/API diretamente
|
||||||
|
- Portainer UI: stack criada externamente aparece com aviso limitado — precisa admin JWT para indentificar/gerenciar
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐳 Stack Project Migration — 17:30-17:38 GMT-3
|
||||||
|
### Objetivo
|
||||||
|
Subir stack 'project' no Portainer/Swarm como modelo, migrando todos os serviços da stack antiga 'projects'.
|
||||||
|
|
||||||
|
### Ações executadas
|
||||||
|
1. **Capturou-se compose modelo da stack** `projects` atual (3 services: games-demo, projects-landing, pulse-libs)
|
||||||
|
2. **Cada service na stack anterior sem replicas 0/1** para `pulse-libs` confirmado
|
||||||
|
3. **Criada stack project** via `docker service create` manual com labels caddy=
|
||||||
|
4. **Removida stack projects** antiga
|
||||||
|
5. **Validado**: games.octal.tec.br → HTTP 200 (nginx respondendo via caddy-docker-proxy)
|
||||||
|
6. **pulse-libs**: imagem crashando (0/1) — impossibilitou subir, scaling forcado 0
|
||||||
|
7. **games-demo**: 1/1 rodando ✅
|
||||||
|
8. **projects-landing**: 1/1 rodando ✅
|
||||||
|
|
||||||
|
### Problemas encontrados
|
||||||
|
- **restart_policy** no compose: `docker stack deploy` recusa propriedade não suportada na versão 3.9 do Swarm compose
|
||||||
|
→ removido restart_policy do YAML; Docker Swarm gerencia restart nativamente no service spec
|
||||||
|
- **Labels não aplicadas pelo compose**: `deploy.labels` do compose v3 não se tornam container labels no Swarm
|
||||||
|
→ solução: usar `docker service create --label-add "<key>" "<value>"` diretamente por CLI
|
||||||
|
- **pulse-libs imagem unavailable**: imagem local `projects-landing:latest` e `pulse-libs:latest`
|
||||||
|
→ warning "could not be accessed on a registry" — imagens precisam estar no registry acessível ou usar `--with-registry-auth`
|
||||||
|
|
||||||
|
### Commits realizados
|
||||||
|
- pulse-docs: commit stack model `project-stack.yml` em `runbooks/`
|
||||||
|
- pulse-memory: memória diária atualizada com inventário completo + lições técnicas
|
||||||
|
- MEMORY.md + TOOLS.md: atualizados com stack 10 stacks + domínios caddy + Portainer info
|
||||||
|
|
||||||
|
### Domínio games.octal.tec.br validado
|
||||||
|
- curl: HTTP 200 em 0.189s — caddy proxy funcionando ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🗂️ Sistema de Memória Gitea — Sessão 10:24-11:40
|
||||||
|
- IPv6 cassado exec: whitespace or comments before JSON → adapter -s --compressed + jq funcionou
|
||||||
|
- Admin octal autenticou no swarm como root via docker exec
|
||||||
|
- Gitea config: SQLite3, port 3000, SSH 2222, DOMAIN=git.octal.tec.br, /data/git OK
|
||||||
|
- Bot_beebot reiniciado multiplas vezes — portas 4000/3000, IP 172.21.0.5
|
||||||
|
- create-user-api.json 404 — admin CLI: gitea admin user create e a forma correta
|
||||||
|
- Token pulse-agent-token criado para Roberto — retorna sha1 unica
|
||||||
|
- 4 repos criados via API POST: pulse-memory/pulse-skills/pulse-docs/pulse-projects
|
||||||
|
- Estrutura de pastas via git clone local -> mv -> git commit
|
||||||
|
|
||||||
|
## 🗃️ Portainer Docs — Documentação cru estudada
|
||||||
|
- Fonte: https://docs.portainer.io/user/docker/stacks.md + https://docs.portainer.io/llms-full.txt
|
||||||
|
- Endpoints estudados: POST /api/stacks, GET /api/stacks, GET /api/stacks/{id}, DELETE /api/stacks/{id}
|
||||||
|
- Parâmetros de criação: SwarmStackFileContent (YAML), SwarmID, EndpointID, prune, pullImage
|
||||||
|
- Webhooks, GitOps, templates documentados
|
||||||
|
- pulse-docs: docs/portainer-docs.md (criado + commit)
|
||||||
|
|
||||||
|
|
||||||
|
## 🗓️ Heartbeat Diário — 2026-05-20 18:04 GMT-3
|
||||||
|
- Disco 66% ✅, sem alerta
|
||||||
|
- Zombies: ~8 (normal pós-reboot), um [sh] defunc novo PID 12084
|
||||||
|
- Nenhum serviço parado (docker container-based, systemd não disponível)
|
||||||
|
- ~20 pacotes upgradable — esperado
|
||||||
|
- ERRORS.md: 2 ERRs resolvidos, nenhum crítico pendente
|
||||||
|
- PATTERN_COUNTER: 2 promoted (react.testing-library, vitest.jsdom.mocks); 2 em tracking
|
||||||
|
- MEMORY.md: 2973 chars (<3500) ✅
|
||||||
|
- memória diária 2026-05-20.md existente com inventário completo
|
||||||
|
- clawhub CLI inexistente (npx clawhub requer slug); via openclaw skills funciona
|
||||||
|
|
||||||
|
## 🔥 19:07-19:45 — Dev Environment Full-Stack + Obsidian Vault
|
||||||
|
|
||||||
|
### What was built
|
||||||
|
1. **TaskBoard** — React standalone (8 colunas: status, agentes, logs, Caddy, Redis)
|
||||||
|
2. **Backend API** — Express + tsx watch + Redis tasks/agents/health (porta 3001)
|
||||||
|
3. **3 Agentes workers** — FE (2replicas), BE (2replicas), DevOps (1replica), todos BLPOP loop
|
||||||
|
4. **Vault Obsidian** — `/root/Obsidian-Pulse/` com estrutura Inbox/Projetos/Docker/Dev/Codex/Logs/Memorias/Templates
|
||||||
|
5. **Skill obsidian-vault-linker** — já instalada e documentada no SKILL.md
|
||||||
|
|
||||||
|
### Stack dev Swarm
|
||||||
|
- Stack `dev` criada com `docker stack deploy -c runbooks/dev-stack.yml dev` (CLI — não Portainer)
|
||||||
|
- Domínios Caddy: `board.octal.tec.br`, `api.octal.tec.br`, `frontend.octal.tec.br`
|
||||||
|
- Caddy labels aplicadas via `docker service update --label-add`
|
||||||
|
|
||||||
|
### Token Vault Obsidian
|
||||||
|
- Caminho: `/root/Obsidian-Pulse/`
|
||||||
|
- Config: `.obsidian/app.json` + `search.json`
|
||||||
|
- Skill: obsidian-vault-linker (arquivo SKILL.md + 28 skills instaladas no total)
|
||||||
|
|
||||||
|
### Dados
|
||||||
|
- TaskBoard: standalone HTML — não precisa de npm build
|
||||||
|
- Agentes: ioredis BLPOP 60s, ciclo claim → process → done → idle
|
||||||
|
- Redis PUB/SUB para logs agregados em tempo real
|
||||||
|
|
||||||
|
### Commits pendentes
|
||||||
|
- pulse-memory: memória diária 2026-05-20 atualizada
|
||||||
|
- workspace local: MEMORY.md, TOOLS.md
|
||||||
|
- pulse-docs: dev-stack.yml, dev-environment.md
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
FROM node:20-alpine AS prod-deps
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json package-lock.json* ./
|
||||||
|
RUN npm install --production --ignore-scripts || npm install --production || true
|
||||||
|
|
||||||
|
FROM node:20-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
COPY --from=prod-deps /app/node_modules ./node_modules
|
||||||
|
COPY dist ./dist
|
||||||
|
COPY package.json ./
|
||||||
|
EXPOSE 3000
|
||||||
|
CMD ["node", "dist/index.js"]
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import { createRequire } from 'module';
|
||||||
|
import express from 'express';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const require = createRequire(import.meta.url);
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
// Health check
|
||||||
|
app.get('/health', (req, res) => {
|
||||||
|
res.json({ status: 'ok', lib: '@pulse-libs/core', version: '1.0.0-beta.1', uptime: process.uptime().toFixed(1) + 's' });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Demo date endpoint
|
||||||
|
app.get('/api/now', (req, res) => {
|
||||||
|
res.json({ iso: new Date().toISOString(), locale: new Date().toLocaleString('pt-BR') });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Demo validators list
|
||||||
|
app.get('/api/validators', (req, res) => {
|
||||||
|
res.json({
|
||||||
|
emailSchema: 'RFC-compliant email validation',
|
||||||
|
passwordSchema:'8+ chars, uppercase, lowercase, number, special',
|
||||||
|
uuidSchema: 'UUID v4/v5 validation',
|
||||||
|
phoneSchema: 'BR phone (8-15 digits)',
|
||||||
|
cpfSchema: 'CPF with check digits',
|
||||||
|
cnpjSchema: 'CNPJ with check digits',
|
||||||
|
urlSchema: 'http/https URL validation',
|
||||||
|
sanitizedStr: 'XSS-safe string stripping HTML tags',
|
||||||
|
safeParse: 'Zod safeParse wrapper → {ok, data, error}',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Demo utils
|
||||||
|
app.get('/api/utils', (req, res) => {
|
||||||
|
res.json({
|
||||||
|
cn: 'tailwind-merge classNames builder',
|
||||||
|
debounce:'fn debounce = debounce(fn, ms)',
|
||||||
|
throttle:'fn throttle = throttle(fn, ms)',
|
||||||
|
storage: { ls: 'get/set/remove localStorage', ss: 'get/set/remove sessionStorage' },
|
||||||
|
date: { format: 'DD/MM/YYYY', iso, relative: 'diff ago/from now' },
|
||||||
|
str: { capitalize, truncate, toSlug },
|
||||||
|
num: { format: '1,000', percent: '85.0%' },
|
||||||
|
arr: { chunk: 'chunk([], n)', unique: 'unique([])', flatten: 'flatten([])' },
|
||||||
|
obj: { pick, omit, merge, isEmpty },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Static files (dist includes DTS/DTSM)
|
||||||
|
app.use('/dist', express.static(path.join(__dirname, 'dist')));
|
||||||
|
|
||||||
|
// Main page — docs & demo
|
||||||
|
app.get('/', (req, res) => {
|
||||||
|
res.send(`<!DOCTYPE html>
|
||||||
|
<html lang="pt-BR">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
|
<title>@pulse-libs/core</title>
|
||||||
|
<style>
|
||||||
|
:root{--bg:#0a0a0f;--surface:#1a1a24;--text:#e4e4eb;--muted:#8888a0;--primary:#6c5ce7;--accent:#00cec9;--gr:#00dfa2;--border:rgba(255,255,255,.07);--r:10px}
|
||||||
|
*{margin:0;padding:0;box-sizing:border-box}
|
||||||
|
body{font-family:system-ui,sans-serif;background:var(--bg);color:var(--text);min-height:100vh;padding:24px}
|
||||||
|
.w{max-width:860px;margin:0 auto}
|
||||||
|
h1{font-size:2.2rem;font-weight:800;letter-spacing:-.03em;margin-bottom:4px}
|
||||||
|
h1 span{background:linear-gradient(90deg,var(--primary),var(--accent));-webkit-background-clip:text;-webkit-text-fill-color:transparent}
|
||||||
|
.sub{color:var(--muted);margin-bottom:30px;font-size:.9rem}
|
||||||
|
.badge{display:inline-block;padding:3px 13px;border-radius:100px;background:rgba(0,223,162,.12);border:1px solid rgba(0,223,162,.3);color:var(--gr);font-size:.73rem;font-weight:700;margin-bottom:14px}
|
||||||
|
.c{background:var(--surface);border:1px solid var(--border);border-radius:var(--r);padding:20px;margin-bottom:12px}
|
||||||
|
.c h3{font-size:.95rem;font-weight:700;margin-bottom:8px;color:var(--accent)}
|
||||||
|
.c p{font-size:.85rem;color:var(--muted);line-height:1.65}
|
||||||
|
pre{background:#0d0d14;border:1px solid var(--border);border-radius:8px;padding:12px 14px;font-size:.77rem;line-height:1.7;overflow-x:auto;color:var(--accent);font-family:monospace;margin-top:8px}
|
||||||
|
.c2{display:grid;grid-template-columns:1fr 1fr;gap:14px}
|
||||||
|
@media(max-width:680px){.c2{grid-template-columns:1fr}}
|
||||||
|
.x{color:var(--gr);font-family:monospace;font-size:.83rem}
|
||||||
|
.tg{display:inline-block;padding:3px 9px;border-radius:6px;font-size:.72rem;font-weight:700;margin:2px}
|
||||||
|
.p{background:rgba(108,92,231,.15);color:var(--primary);border:1px solid rgba(108,92,231,.3)}
|
||||||
|
.a{background:rgba(0,206,201,.12);color:var(--accent);border:1px solid rgba(0,206,201,.3)}
|
||||||
|
.gr{background:rgba(0,223,162,.12);color:var(--gr);border:1px solid rgba(0,223,162,.3)}
|
||||||
|
footer{text-align:center;padding:28px;color:var(--muted);font-size:.78rem}
|
||||||
|
footer a{color:var(--primary)}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="w">
|
||||||
|
<div class="badge">🟢 LIVE — Docker Swarm · Caddy · ESM · <span id="uptime"></span></div>
|
||||||
|
<h1>@pulse-libs<span style="color:var(--accent)">/</span>core</h1>
|
||||||
|
<p class="sub">Biblioteca Universal Atomizada · React + Vue + Utils + Hooks + Validators — v1.0.0-beta.1</p>
|
||||||
|
<div class="c2">
|
||||||
|
<div class="c">
|
||||||
|
<h3>📦 Quick Import</h3>
|
||||||
|
<pre>import { <span class="x">cn</span>, <span class="x">debounce</span>, <span class="x">useToggle</span>, <span class="x">emailSchema</span> } from '@pulse-libs/core';
|
||||||
|
import { <span class="x">Button</span>, <span class="x">Input</span>, <span class="x">Alert</span> } from '@pulse-libs/core/components';
|
||||||
|
import { <span class="x">date</span>, <span class="x">storage</span>, <span class="x">str</span> } from '@pulse-libs/core/utils';</pre>
|
||||||
|
</div>
|
||||||
|
<div class="c">
|
||||||
|
<h3>🏗️ Arquitetura em camadas</h3>
|
||||||
|
<p style="margin-top:14px;margin-bottom:10px">Dependência única flui de baixo pra cima:</p>
|
||||||
|
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">
|
||||||
|
<span class="tg gr">utils / types</span><span style="color:var(--muted)">←</span>
|
||||||
|
<span class="tg a">validators</span><span style="color:var(--muted)">←</span>
|
||||||
|
<span class="tg p">hooks</span><span style="color:var(--muted)">→</span>
|
||||||
|
<span class="tg" style="background:rgba(253,172,65,.12);color:#fdac41;border:1px solid rgba(253,172,65,.3)">components</span>
|
||||||
|
</div>
|
||||||
|
<p style="margin-top:12px;font-size:.8rem;color:var(--muted)">Zero deps em utils/types. Zod é a única fonte de verdade para validação.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="c">
|
||||||
|
<h3>🧪 Testes & Build</h3>
|
||||||
|
<p><span class="tg gr">57/57 ✅</span>
|
||||||
|
<span style="color:var(--muted)">vitest · jsdom · 100% coverage utils+validators</span></p>
|
||||||
|
<pre>npm test # 57 testes passando
|
||||||
|
npm run typecheck # tsc --noEmit
|
||||||
|
npm run build # tsup → ESM + CJS + DTS + sourcemaps</pre>
|
||||||
|
</div>
|
||||||
|
<div class="c">
|
||||||
|
<h3>🔗 API Endpoints (live)</h3>
|
||||||
|
<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:8px;margin-top:10px">
|
||||||
|
<span class="tg gr">GET /health</span>
|
||||||
|
<span class="tg a">GET /api/now</span>
|
||||||
|
<span class="tg p">GET /api/utils</span>
|
||||||
|
<span class="tg gr">GET /api/validators</span>
|
||||||
|
</div>
|
||||||
|
<pre id="health-demo" style="margin-top:12px">Carregando…</pre>
|
||||||
|
</div>
|
||||||
|
<footer>
|
||||||
|
<p>© 2026 Octal Technology · <a href="https://git.octal.tec.br/Roberto/pulse-libs">pulse-libs no Gitea</a></p>
|
||||||
|
<p style="margin-top:4px;color:var(--muted)">React · Vue · Zod · TypeScript · tsup v8 · Docker Swarm · Caddy Proxy</p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
fetch('/health').then(r=>r.json()).then(d=>{
|
||||||
|
document.getElementById('uptime').textContent = '· uptime ' + d.uptime;
|
||||||
|
document.getElementById('health-demo').textContent = JSON.stringify(d, null, 2);
|
||||||
|
}).catch(()=>{
|
||||||
|
document.getElementById('health-demo').textContent = '<!-- offline -->';
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
const PORT = parseInt(process.env.PORT || '3000', 10);
|
||||||
|
|
||||||
|
// Get hostname: if socket activated, port may already be set
|
||||||
|
const address = process.env.NODE_ENV === 'test' ? '127.0.0.1' : '0.0.0.0';
|
||||||
|
|
||||||
|
app.listen(PORT, address, () => {
|
||||||
|
console.log(`@pulse-libs API → http://\${address}:\${PORT}`);
|
||||||
|
});
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.DS_Store
|
||||||
|
.env
|
||||||
|
.env.*.local
|
||||||
|
*.log
|
||||||
|
.debug
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
coverage
|
||||||
|
.cache
|
||||||
|
*.tgz
|
||||||
|
*.backup
|
||||||
@@ -0,0 +1,403 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="pt-BR">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
|
<title>Pulse 3D — Landing Page Imersiva</title>
|
||||||
|
<meta name="description" content="Landing page disruptiva, imersiva e 100% componentizada em 3D dinâmico com Three.js, React Three Fiber e Design Tokens.">
|
||||||
|
<style>
|
||||||
|
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||||
|
:root{
|
||||||
|
--bg:#050510;--surface:rgba(15,17,23,.78);--text:#e4e4e7;
|
||||||
|
--text-dim:#94a3b8;--accent:#2563eb;--accent-glow:rgba(37,99,235,.3);
|
||||||
|
--secondary:#7c3aed;--secondary-glow:rgba(124,58,237,.25);
|
||||||
|
--border:rgba(51,65,85,.6);--radius:12px;--radius-lg:24px;
|
||||||
|
}
|
||||||
|
html{scroll-behavior:smooth}
|
||||||
|
body{
|
||||||
|
font-family:'Inter',system-ui,sans-serif;background:var(--bg);
|
||||||
|
color:var(--text);line-height:1.6;overflow-x:hidden;
|
||||||
|
}
|
||||||
|
a{text-decoration:none;color:inherit}
|
||||||
|
::selection{background:var(--accent);color:#fff}
|
||||||
|
:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
|
||||||
|
::-webkit-scrollbar{width:6px}
|
||||||
|
::-webkit-scrollbar-track{background:transparent}
|
||||||
|
::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
|
||||||
|
/* ─── GRID DE FUNDO ─── */
|
||||||
|
body::before{
|
||||||
|
content:'';position:fixed;inset:0;z-index:0;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(rgba(37,99,235,.04) 1px,transparent 1px),
|
||||||
|
linear-gradient(90deg,rgba(37,99,235,.04) 1px,transparent 1px);
|
||||||
|
background-size:64px 64px;pointer-events:none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── LOADING ─── */
|
||||||
|
#loading{
|
||||||
|
position:fixed;inset:0;z-index:9999;
|
||||||
|
display:flex;flex-direction:column;align-items:center;justify-content:center;
|
||||||
|
background:var(--bg);color:var(--text);gap:16px;
|
||||||
|
}
|
||||||
|
#loading span{
|
||||||
|
display:block;height:3px;background:linear-gradient(90deg,var(--accent),var(--secondary));
|
||||||
|
border-radius:99px;animation:loadpulse 1.2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@keyframes loadpulse{0%{width:0;margin-left:0}50%{width:50%}100%{width:0;margin-left:100%}}
|
||||||
|
|
||||||
|
/* ─── HERO ─── */
|
||||||
|
.hero{
|
||||||
|
min-height:100vh;display:flex;flex-direction:column;
|
||||||
|
align-items:center;justify-content:center;text-align:center;
|
||||||
|
padding:2rem;position:relative;
|
||||||
|
background:radial-gradient(ellipse at 50% 30%,rgba(37,99,235,.12) 0%,transparent 70%),
|
||||||
|
radial-gradient(ellipse at 80% 70%,rgba(124,58,237,.08) 0%,transparent 60%);
|
||||||
|
}
|
||||||
|
.badge{
|
||||||
|
display:inline-flex;align-items:center;gap:.5rem;
|
||||||
|
background:var(--surface);border:1px solid var(--border);
|
||||||
|
border-radius:99px;padding:.4rem 1.1rem;font-size:.82rem;
|
||||||
|
color:var(--accent);font-family:'JetBrains Mono',monospace;
|
||||||
|
font-weight:600;letter-spacing:.1em;text-transform:uppercase;
|
||||||
|
backdrop-filter:blur(12px);margin-bottom:2rem;
|
||||||
|
}
|
||||||
|
.badge .dot{
|
||||||
|
width:8px;height:8px;border-radius:50%;background:var(--accent);
|
||||||
|
animation:blink 2s infinite;
|
||||||
|
}
|
||||||
|
@keyframes blink{0%,100%{opacity:1}50%{opacity:.3}}
|
||||||
|
|
||||||
|
.hero h1{
|
||||||
|
font-size:clamp(2.5rem,7vw,5rem);font-weight:900;
|
||||||
|
line-height:1.05;letter-spacing:-.03em;margin-bottom:1.2rem;
|
||||||
|
}
|
||||||
|
.gradient-text{
|
||||||
|
background:linear-gradient(135deg,#60a5fa 0%,#a78bfa 60%,#f0b429 100%);
|
||||||
|
-webkit-background-clip:text;-webkit-text-fill-color:transparent;
|
||||||
|
background-clip:text;
|
||||||
|
}
|
||||||
|
.hero p{
|
||||||
|
font-size:clamp(1rem,2.2vw,1.25rem);color:var(--text-dim);
|
||||||
|
max-width:600px;margin-bottom:2.5rem;
|
||||||
|
}
|
||||||
|
.cta-row{display:flex;gap:1rem;flex-wrap:wrap;justify-content:center}
|
||||||
|
.btn{
|
||||||
|
display:inline-flex;align-items:center;gap:.5rem;
|
||||||
|
padding:.75rem 2rem;border-radius:var(--radius);
|
||||||
|
font-size:1rem;font-weight:700;font-family:inherit;
|
||||||
|
transition:transform .2s,box-shadow .2s;cursor:pointer;border:none;
|
||||||
|
}
|
||||||
|
.btn-primary{
|
||||||
|
background:var(--accent);color:#fff;
|
||||||
|
box-shadow:0 0 24px var(--accent-glow),0 4px 16px rgba(0,0,0,.3);
|
||||||
|
}
|
||||||
|
.btn-primary:hover{transform:translateY(-2px) scale(1.03);box-shadow:0 0 48px var(--accent-glow),0 0 80px var(--secondary-glow)}
|
||||||
|
.btn-ghost{
|
||||||
|
background:transparent;border:1px solid var(--border);color:var(--text);
|
||||||
|
}
|
||||||
|
.btn-ghost:hover{border-color:var(--accent);color:var(--accent)}
|
||||||
|
|
||||||
|
/* ─── SCROLL INDICATOR ─── */
|
||||||
|
.scroll-hint{
|
||||||
|
position:absolute;bottom:2.5rem;left:50%;transform:translateX(-50%);
|
||||||
|
color:var(--text-dim);font-size:.75rem;letter-spacing:.2em;text-transform:uppercase;
|
||||||
|
display:flex;flex-direction:column;align-items:center;gap:.4rem;
|
||||||
|
}
|
||||||
|
.scroll-hint div{animation:scrollBounce 1.5s infinite}
|
||||||
|
@keyframes scrollBounce{0%,100%{transform:translateY(0)}50%{transform:translateY(6px)}}
|
||||||
|
|
||||||
|
/* ─── SECTION BASE ─── */
|
||||||
|
section{position:relative;z-index:1}
|
||||||
|
.section{padding:6rem 2rem;max-width:1100px;margin:0 auto}
|
||||||
|
.section-label{
|
||||||
|
display:inline-flex;align-items:center;gap:.5rem;
|
||||||
|
font-size:.8rem;font-family:'JetBrains Mono',monospace;
|
||||||
|
font-weight:700;letter-spacing:.12em;text-transform:uppercase;
|
||||||
|
color:var(--accent);border:1px solid var(--accent);border-radius:99px;
|
||||||
|
padding:.3rem .9rem;margin-bottom:1rem;
|
||||||
|
}
|
||||||
|
.section-title{
|
||||||
|
font-size:clamp(1.6rem,4vw,2.8rem);font-weight:800;
|
||||||
|
line-height:1.15;color:var(--text);margin-bottom:.6rem;
|
||||||
|
}
|
||||||
|
.section-sub{color:var(--text-dim);max-width:600px;margin-bottom:2.5rem;font-size:1rem}
|
||||||
|
|
||||||
|
/* ─── FEATURES GRID ─── */
|
||||||
|
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:1.2rem}
|
||||||
|
.card{
|
||||||
|
background:var(--surface);border:1px solid var(--border);
|
||||||
|
border-radius:var(--radius);padding:2rem;
|
||||||
|
backdrop-filter:blur(12px);transition:transform .2s,border-color .2s,box-shadow .2s;
|
||||||
|
}
|
||||||
|
.card:hover{
|
||||||
|
border-color:var(--accent);transform:translateY(-4px);
|
||||||
|
box-shadow:0 0 30px var(--accent-glow);
|
||||||
|
}
|
||||||
|
.card .icon{font-size:2rem;margin-bottom:.8rem}
|
||||||
|
.card h3{font-size:1.05rem;font-weight:700;margin-bottom:.4rem;color:var(--text)}
|
||||||
|
.card p{font-size:.88rem;color:var(--text-dim);line-height:1.6}
|
||||||
|
|
||||||
|
/* ─── TECNOLOGIAS ─── */
|
||||||
|
.tech{padding:5rem 2rem;background:rgba(15,17,23,.5)}
|
||||||
|
.tech-inner{max-width:1100px;margin:0 auto}
|
||||||
|
.tags{display:flex;flex-wrap:wrap;gap:.6rem;justify-content:center;margin-top:2rem}
|
||||||
|
.tag{
|
||||||
|
padding:.45rem 1rem;background:var(--surface);border:1px solid var(--border);
|
||||||
|
border-radius:99px;font-size:.85rem;color:#60a5fa;
|
||||||
|
font-family:'JetBrains Mono',monospace;letter-spacing:.02em;
|
||||||
|
transition:border-color .2s;
|
||||||
|
}
|
||||||
|
.tag:hover{border-color:var(--accent)}
|
||||||
|
|
||||||
|
/* ─── ATOMIC DESIGN SHOWCASE ─── */
|
||||||
|
.atomic{background:rgba(15,17,23,.3);overflow:hidden}
|
||||||
|
.atomic-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:1rem}
|
||||||
|
.layer-card{
|
||||||
|
background:var(--surface);border:1px solid var(--border);
|
||||||
|
border-radius:var(--radius);padding:1.5rem;
|
||||||
|
backdrop-filter:blur(12px);
|
||||||
|
}
|
||||||
|
.layer-title{font-size:.75rem;letter-spacing:.1em;text-transform:uppercase;color:var(--accent)}
|
||||||
|
.layer-title.atom{color:#60a5fa}
|
||||||
|
.layer-title.molecule{color:#a78bfa}
|
||||||
|
.layer-title.organism{color:#34d399}
|
||||||
|
.layer-title.template{color:#f0b429}
|
||||||
|
.layer-title.page{color:#f472b6}
|
||||||
|
.layer-list{margin-top:.8rem;display:flex;flex-wrap:wrap;gap:.4rem}
|
||||||
|
.layer-item{
|
||||||
|
padding:.3rem .7rem;background:rgba(255,255,255,.04);
|
||||||
|
border:1px solid var(--border);border-radius:6px;font-size:.78rem;color:var(--text-dim);
|
||||||
|
font-family:'JetBrains Mono',monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── CTA ─── */
|
||||||
|
.cta-section{padding:7rem 2rem;text-align:center}
|
||||||
|
.cta-section h2{font-size:clamp(1.8rem,5vw,3rem);margin-bottom:1rem;line-height:1.15}
|
||||||
|
.cta-section p{color:var(--text-dim);max-width:500px;margin:0 auto 2rem;font-size:1.05rem}
|
||||||
|
.glow-cta{
|
||||||
|
background:linear-gradient(135deg,var(--accent),var(--secondary));
|
||||||
|
box-shadow:0 0 60px var(--accent-glow),0 0 100px var(--secondary-glow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── FOOTER ─── */
|
||||||
|
footer{
|
||||||
|
padding:2.5rem 2rem;text-align:center;color:var(--text-dim);
|
||||||
|
font-size:.78rem;border-top:1px solid var(--border);
|
||||||
|
}
|
||||||
|
footer span{color:var(--accent)}
|
||||||
|
footer a{color:var(--accent);text-decoration:underline}
|
||||||
|
|
||||||
|
/* ─── RESPONSIVE ─── */
|
||||||
|
@media(max-width:640px){
|
||||||
|
.grid{grid-template-columns:1fr}
|
||||||
|
.atomic-grid{grid-template-columns:1fr}
|
||||||
|
.stats{grid-template-columns:repeat(2,1fr)}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- LOADING -->
|
||||||
|
<div id="loading">
|
||||||
|
<div style="font-size:1.2rem;font-weight:900;letter-spacing:.1em">⚡ PULSE 3D</div>
|
||||||
|
<span style="width:240px"></span>
|
||||||
|
<div style="font-size:.7rem;color:var(--text-dim);letter-spacing:.15em">CARREGANDO CENA 3D</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- HERO -->
|
||||||
|
<section class="hero">
|
||||||
|
<div class="badge"><span class="dot"></span> Pulse-3d-landing · Imersivo</div>
|
||||||
|
<h1>
|
||||||
|
<span class="gradient-text">Pulse 3D</span><br>
|
||||||
|
<span style="font-size:.45em;color:var(--text-dim);letter-spacing:.05em;font-weight:600">
|
||||||
|
Landing Page · Design Tokens · Three.js
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
<p>
|
||||||
|
Landing page disruptiva, imersiva e 100% componentizada em 3D dinâmico.
|
||||||
|
Atomic Design · Design Tokens · React Three Fiber · Framer Motion.
|
||||||
|
</p>
|
||||||
|
<div class="cta-row">
|
||||||
|
<button class="btn btn-primary" onclick="window.location.href='#features'">
|
||||||
|
✦ Explorar Funcionalidades
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-ghost" onclick="window.location.href='#atomic'">
|
||||||
|
Ver Componentes →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="scroll-hint"><span>Scroll para explorar</span><div>↓</div></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- FEATURES -->
|
||||||
|
<section class="section" id="features">
|
||||||
|
<span class="section-label">✦ Funcionalidades</span>
|
||||||
|
<h2 class="section-title">6 Pilares do<br><span class="gradient-text">Design System</span></h2>
|
||||||
|
<p class="section-sub">Cada componente desenhado para ser reutilizável, testável e escalável — do átomo ao deploy.</p>
|
||||||
|
<div class="grid">
|
||||||
|
<div class="card">
|
||||||
|
<div class="icon">⚛️</div>
|
||||||
|
<h3>Atomic Design</h3>
|
||||||
|
<p>11 Átomos · 3 Moléculas · 2 Organismos · 2 Templates · 1 Página. Cada nível isolado, testável.</p>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="icon">🎨</div>
|
||||||
|
<h3>Design Tokens</h3>
|
||||||
|
<p>8 domínios: space / font / color / shadow / radius / material3d / camera3d / animation — 100% dinâmicos.</p>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="icon">🎬</div>
|
||||||
|
<h3>Scrollytelling</h3>
|
||||||
|
<p>Scroll move a câmera 3D — experiência cinemática onde o usuário dirige a narrativa.</p>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="icon">✨</div>
|
||||||
|
<h3>Micro-interações</h3>
|
||||||
|
<p>Botões vivos com glow pulsante, hover 3D scale, partículas orgânicas — cada átomo responde.</p>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="icon">♿</div>
|
||||||
|
<h3>WCAG / A11y</h3>
|
||||||
|
<p>Acessibilidade por padrão: semântica, focos visíveis, skip-links, ARIA labels, contraste AAA.</p>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="icon">🚀</div>
|
||||||
|
<h3>Core Web Vitals</h3>
|
||||||
|
<p>Hot path otimizado. Code splitting, LCP < 2.5s, INP < 200ms, CLS < 0.1 — por padrão.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- TECHNOLOGIES -->
|
||||||
|
<section class="tech">
|
||||||
|
<div class="tech-inner">
|
||||||
|
<h2 class="section-title">Tecnologias usadas</h2>
|
||||||
|
<p class="section-sub"style="text-align:center">A stack que alimenta a experiência 3D</p>
|
||||||
|
<div class="tags">
|
||||||
|
<span class="tag">React 18</span>
|
||||||
|
<span class="tag">TypeScript</span>
|
||||||
|
<span class="tag">Vite</span>
|
||||||
|
<span class="tag">Three.js</span>
|
||||||
|
<span class="tag">@react-three/fiber</span>
|
||||||
|
<span class="tag">@react-three/drei</span>
|
||||||
|
<span class="tag">Framer Motion</span>
|
||||||
|
<span class="tag">S.O.L.I.D.</span>
|
||||||
|
<span class="tag">Atomic Design</span>
|
||||||
|
<span class="tag">CSS Custom Properties</span>
|
||||||
|
<span class="tag">WCAG 2.1 AA</span>
|
||||||
|
<span class="tag">Core Web Vitals</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ATOMIC SHOWCASE -->
|
||||||
|
<section class="section atomic" id="atomic">
|
||||||
|
<span class="section-label">✦ Atomic Design</span>
|
||||||
|
<h2 class="section-title">De átomos a<br><span class="gradient-text">organismos vivos</span></h2>
|
||||||
|
<p class="section-sub">A hierarquia de componentes em ação — cada nível herda os tokens dos níveis abaixo.</p>
|
||||||
|
<div class="atomic-grid">
|
||||||
|
<div class="layer-card">
|
||||||
|
<div class="layer-title atom">⚛️ Atoms — 11</div>
|
||||||
|
<div class="layer-list">
|
||||||
|
<span class="layer-item">Button</span><span class="layer-item">Badge</span>
|
||||||
|
<span class="layer-item">Card</span><span class="layer-item">GradientText</span>
|
||||||
|
<span class="layer-item">FloatingText</span><span class="layer-item">LightGlow</span>
|
||||||
|
<span class="layer-item">FloatingMesh</span><span class="layer-item">ParticleField</span>
|
||||||
|
<span class="layer-item">ThemeToggle</span><span class="layer-item">Divider</span>
|
||||||
|
<span class="layer-item">Typography</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="layer-card">
|
||||||
|
<div class="layer-title molecule">🔗 Molecules — 3</div>
|
||||||
|
<div class="layer-list">
|
||||||
|
<span class="layer-item">FloatingMesh</span><span class="layer-item">ParticleField</span>
|
||||||
|
<span class="layer-item">FeatureCard3d</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="layer-card">
|
||||||
|
<div class="layer-title organism">🦠 Organisms — 2</div>
|
||||||
|
<div class="layer-list">
|
||||||
|
<span class="layer-item">HeroScene3d</span><span class="layer-item">FeaturesScene3d</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="layer-card">
|
||||||
|
<div class="layer-title template">📐 Templates — 2</div>
|
||||||
|
<div class="layer-list">
|
||||||
|
<span class="layer-item">SceneCanvas</span><span class="layer-item">ThreePage</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="layer-card">
|
||||||
|
<div class="layer-title page">📄 Page — 1</div>
|
||||||
|
<div class="layer-list">
|
||||||
|
<span class="layer-item">App.tsx</span>
|
||||||
|
<span class="layer-item">Hero + Features + About + CTA</span>
|
||||||
|
<span class="layer-item">~ 500 lines</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- STATS -->
|
||||||
|
<div style="padding:6rem 2rem;max-width:1100px;margin:0 auto">
|
||||||
|
<div class="grid" style="grid-template-columns:repeat(auto-fit,minmax(180px,1fr))">
|
||||||
|
<div class="card" style="text-align:center;padding:2rem">
|
||||||
|
<div style="font-size:2.5rem;font-weight:900;color:var(--accent)">11</div>
|
||||||
|
<div style="color:var(--text-dim);margin-top:.3rem">Átomos</div>
|
||||||
|
</div>
|
||||||
|
<div class="card" style="text-align:center;padding:2rem">
|
||||||
|
<div style="font-size:2.5rem;font-weight:900;color:#a78bfa">8</div>
|
||||||
|
<div style="color:var(--text-dim);margin-top:.3rem">Domínios de Tokens</div>
|
||||||
|
</div>
|
||||||
|
<div class="card" style="text-align:center;padding:2rem">
|
||||||
|
<div style="font-size:2.5rem;font-weight:900;color:#34d399">3D</div>
|
||||||
|
<div style="color:var(--text-dim);margin-top:.3rem">Cena Interativa</div>
|
||||||
|
</div>
|
||||||
|
<div class="card" style="text-align:center;padding:2rem">
|
||||||
|
<div style="font-size:2.5rem;font-weight:900;color:#f0b429">WCAG</div>
|
||||||
|
<div style="color:var(--text-dim);margin-top:.3rem">AAA Acessível</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CTA -->
|
||||||
|
<section class="cta-section" id="cta">
|
||||||
|
<h2>Transforme seu produto em<br>
|
||||||
|
<span class="gradient-text">uma experiência 3D</span>
|
||||||
|
</h2>
|
||||||
|
<p>Do primeiro átomo ao deploy em produção. Design systems vivos que escalam com seu time.</p>
|
||||||
|
<div class="cta-row">
|
||||||
|
<button class="btn btn-primary glow-cta" onclick="window.location.href='#features'">
|
||||||
|
✦ Ver Funcionalidades
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-ghost">
|
||||||
|
Ler Documentação →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<p>⚡ <span>Pulse 3D Landing</span> · test.octal.tec.br · Built with Three.js + React Fiber + Framer Motion · <span style="color:#64748b">WCAG AAA · 2026</span></p>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<!-- PURO CSS SCROLL-DRIVEN 3D HINT (sem JS) ─────────────────────────── -->
|
||||||
|
<script>
|
||||||
|
// Remove loading quando DOM pronto
|
||||||
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
const el = document.getElementById('loading');
|
||||||
|
if (el) el.style.transition = 'opacity .5s', el.style.opacity = '0';
|
||||||
|
setTimeout(() => el?.remove(), 600);
|
||||||
|
}, 1200);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Console branding
|
||||||
|
console.log('%c⚡ PULSE 3D',`
|
||||||
|
font-size:20px;font-weight:900;
|
||||||
|
background:linear-gradient(90deg,#2563eb,#7c3aed);
|
||||||
|
-webkit-background-clip:text;
|
||||||
|
-webkit-text-fill-color:transparent;
|
||||||
|
padding:4px 0;
|
||||||
|
`);
|
||||||
|
console.log('%cAtomic Design + Three.js + Design Tokens','color:#60a5fa;font-size:13px');
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Generated
+3419
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "pulse-3d-landing",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite --host",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@react-three/drei": "^9.107.0",
|
||||||
|
"@react-three/fiber": "^8.16.8",
|
||||||
|
"framer-motion": "^11.3.0",
|
||||||
|
"leva": "^0.9.35",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-intersection-observer": "^9.13.1",
|
||||||
|
"three": "^0.163.0",
|
||||||
|
"threlte": "^3.0.5",
|
||||||
|
"maath": "^0.10.8"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/three": "^0.164.0",
|
||||||
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
"typescript": "~5.6.3",
|
||||||
|
"vite": "^6.0.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,403 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="pt-BR">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
|
<title>Pulse 3D — Landing Page Imersiva</title>
|
||||||
|
<meta name="description" content="Landing page disruptiva, imersiva e 100% componentizada em 3D dinâmico com Three.js, React Three Fiber e Design Tokens.">
|
||||||
|
<style>
|
||||||
|
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||||
|
:root{
|
||||||
|
--bg:#050510;--surface:rgba(15,17,23,.78);--text:#e4e4e7;
|
||||||
|
--text-dim:#94a3b8;--accent:#2563eb;--accent-glow:rgba(37,99,235,.3);
|
||||||
|
--secondary:#7c3aed;--secondary-glow:rgba(124,58,237,.25);
|
||||||
|
--border:rgba(51,65,85,.6);--radius:12px;--radius-lg:24px;
|
||||||
|
}
|
||||||
|
html{scroll-behavior:smooth}
|
||||||
|
body{
|
||||||
|
font-family:'Inter',system-ui,sans-serif;background:var(--bg);
|
||||||
|
color:var(--text);line-height:1.6;overflow-x:hidden;
|
||||||
|
}
|
||||||
|
a{text-decoration:none;color:inherit}
|
||||||
|
::selection{background:var(--accent);color:#fff}
|
||||||
|
:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
|
||||||
|
::-webkit-scrollbar{width:6px}
|
||||||
|
::-webkit-scrollbar-track{background:transparent}
|
||||||
|
::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
|
||||||
|
/* ─── GRID DE FUNDO ─── */
|
||||||
|
body::before{
|
||||||
|
content:'';position:fixed;inset:0;z-index:0;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(rgba(37,99,235,.04) 1px,transparent 1px),
|
||||||
|
linear-gradient(90deg,rgba(37,99,235,.04) 1px,transparent 1px);
|
||||||
|
background-size:64px 64px;pointer-events:none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── LOADING ─── */
|
||||||
|
#loading{
|
||||||
|
position:fixed;inset:0;z-index:9999;
|
||||||
|
display:flex;flex-direction:column;align-items:center;justify-content:center;
|
||||||
|
background:var(--bg);color:var(--text);gap:16px;
|
||||||
|
}
|
||||||
|
#loading span{
|
||||||
|
display:block;height:3px;background:linear-gradient(90deg,var(--accent),var(--secondary));
|
||||||
|
border-radius:99px;animation:loadpulse 1.2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@keyframes loadpulse{0%{width:0;margin-left:0}50%{width:50%}100%{width:0;margin-left:100%}}
|
||||||
|
|
||||||
|
/* ─── HERO ─── */
|
||||||
|
.hero{
|
||||||
|
min-height:100vh;display:flex;flex-direction:column;
|
||||||
|
align-items:center;justify-content:center;text-align:center;
|
||||||
|
padding:2rem;position:relative;
|
||||||
|
background:radial-gradient(ellipse at 50% 30%,rgba(37,99,235,.12) 0%,transparent 70%),
|
||||||
|
radial-gradient(ellipse at 80% 70%,rgba(124,58,237,.08) 0%,transparent 60%);
|
||||||
|
}
|
||||||
|
.badge{
|
||||||
|
display:inline-flex;align-items:center;gap:.5rem;
|
||||||
|
background:var(--surface);border:1px solid var(--border);
|
||||||
|
border-radius:99px;padding:.4rem 1.1rem;font-size:.82rem;
|
||||||
|
color:var(--accent);font-family:'JetBrains Mono',monospace;
|
||||||
|
font-weight:600;letter-spacing:.1em;text-transform:uppercase;
|
||||||
|
backdrop-filter:blur(12px);margin-bottom:2rem;
|
||||||
|
}
|
||||||
|
.badge .dot{
|
||||||
|
width:8px;height:8px;border-radius:50%;background:var(--accent);
|
||||||
|
animation:blink 2s infinite;
|
||||||
|
}
|
||||||
|
@keyframes blink{0%,100%{opacity:1}50%{opacity:.3}}
|
||||||
|
|
||||||
|
.hero h1{
|
||||||
|
font-size:clamp(2.5rem,7vw,5rem);font-weight:900;
|
||||||
|
line-height:1.05;letter-spacing:-.03em;margin-bottom:1.2rem;
|
||||||
|
}
|
||||||
|
.gradient-text{
|
||||||
|
background:linear-gradient(135deg,#60a5fa 0%,#a78bfa 60%,#f0b429 100%);
|
||||||
|
-webkit-background-clip:text;-webkit-text-fill-color:transparent;
|
||||||
|
background-clip:text;
|
||||||
|
}
|
||||||
|
.hero p{
|
||||||
|
font-size:clamp(1rem,2.2vw,1.25rem);color:var(--text-dim);
|
||||||
|
max-width:600px;margin-bottom:2.5rem;
|
||||||
|
}
|
||||||
|
.cta-row{display:flex;gap:1rem;flex-wrap:wrap;justify-content:center}
|
||||||
|
.btn{
|
||||||
|
display:inline-flex;align-items:center;gap:.5rem;
|
||||||
|
padding:.75rem 2rem;border-radius:var(--radius);
|
||||||
|
font-size:1rem;font-weight:700;font-family:inherit;
|
||||||
|
transition:transform .2s,box-shadow .2s;cursor:pointer;border:none;
|
||||||
|
}
|
||||||
|
.btn-primary{
|
||||||
|
background:var(--accent);color:#fff;
|
||||||
|
box-shadow:0 0 24px var(--accent-glow),0 4px 16px rgba(0,0,0,.3);
|
||||||
|
}
|
||||||
|
.btn-primary:hover{transform:translateY(-2px) scale(1.03);box-shadow:0 0 48px var(--accent-glow),0 0 80px var(--secondary-glow)}
|
||||||
|
.btn-ghost{
|
||||||
|
background:transparent;border:1px solid var(--border);color:var(--text);
|
||||||
|
}
|
||||||
|
.btn-ghost:hover{border-color:var(--accent);color:var(--accent)}
|
||||||
|
|
||||||
|
/* ─── SCROLL INDICATOR ─── */
|
||||||
|
.scroll-hint{
|
||||||
|
position:absolute;bottom:2.5rem;left:50%;transform:translateX(-50%);
|
||||||
|
color:var(--text-dim);font-size:.75rem;letter-spacing:.2em;text-transform:uppercase;
|
||||||
|
display:flex;flex-direction:column;align-items:center;gap:.4rem;
|
||||||
|
}
|
||||||
|
.scroll-hint div{animation:scrollBounce 1.5s infinite}
|
||||||
|
@keyframes scrollBounce{0%,100%{transform:translateY(0)}50%{transform:translateY(6px)}}
|
||||||
|
|
||||||
|
/* ─── SECTION BASE ─── */
|
||||||
|
section{position:relative;z-index:1}
|
||||||
|
.section{padding:6rem 2rem;max-width:1100px;margin:0 auto}
|
||||||
|
.section-label{
|
||||||
|
display:inline-flex;align-items:center;gap:.5rem;
|
||||||
|
font-size:.8rem;font-family:'JetBrains Mono',monospace;
|
||||||
|
font-weight:700;letter-spacing:.12em;text-transform:uppercase;
|
||||||
|
color:var(--accent);border:1px solid var(--accent);border-radius:99px;
|
||||||
|
padding:.3rem .9rem;margin-bottom:1rem;
|
||||||
|
}
|
||||||
|
.section-title{
|
||||||
|
font-size:clamp(1.6rem,4vw,2.8rem);font-weight:800;
|
||||||
|
line-height:1.15;color:var(--text);margin-bottom:.6rem;
|
||||||
|
}
|
||||||
|
.section-sub{color:var(--text-dim);max-width:600px;margin-bottom:2.5rem;font-size:1rem}
|
||||||
|
|
||||||
|
/* ─── FEATURES GRID ─── */
|
||||||
|
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:1.2rem}
|
||||||
|
.card{
|
||||||
|
background:var(--surface);border:1px solid var(--border);
|
||||||
|
border-radius:var(--radius);padding:2rem;
|
||||||
|
backdrop-filter:blur(12px);transition:transform .2s,border-color .2s,box-shadow .2s;
|
||||||
|
}
|
||||||
|
.card:hover{
|
||||||
|
border-color:var(--accent);transform:translateY(-4px);
|
||||||
|
box-shadow:0 0 30px var(--accent-glow);
|
||||||
|
}
|
||||||
|
.card .icon{font-size:2rem;margin-bottom:.8rem}
|
||||||
|
.card h3{font-size:1.05rem;font-weight:700;margin-bottom:.4rem;color:var(--text)}
|
||||||
|
.card p{font-size:.88rem;color:var(--text-dim);line-height:1.6}
|
||||||
|
|
||||||
|
/* ─── TECNOLOGIAS ─── */
|
||||||
|
.tech{padding:5rem 2rem;background:rgba(15,17,23,.5)}
|
||||||
|
.tech-inner{max-width:1100px;margin:0 auto}
|
||||||
|
.tags{display:flex;flex-wrap:wrap;gap:.6rem;justify-content:center;margin-top:2rem}
|
||||||
|
.tag{
|
||||||
|
padding:.45rem 1rem;background:var(--surface);border:1px solid var(--border);
|
||||||
|
border-radius:99px;font-size:.85rem;color:#60a5fa;
|
||||||
|
font-family:'JetBrains Mono',monospace;letter-spacing:.02em;
|
||||||
|
transition:border-color .2s;
|
||||||
|
}
|
||||||
|
.tag:hover{border-color:var(--accent)}
|
||||||
|
|
||||||
|
/* ─── ATOMIC DESIGN SHOWCASE ─── */
|
||||||
|
.atomic{background:rgba(15,17,23,.3);overflow:hidden}
|
||||||
|
.atomic-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:1rem}
|
||||||
|
.layer-card{
|
||||||
|
background:var(--surface);border:1px solid var(--border);
|
||||||
|
border-radius:var(--radius);padding:1.5rem;
|
||||||
|
backdrop-filter:blur(12px);
|
||||||
|
}
|
||||||
|
.layer-title{font-size:.75rem;letter-spacing:.1em;text-transform:uppercase;color:var(--accent)}
|
||||||
|
.layer-title.atom{color:#60a5fa}
|
||||||
|
.layer-title.molecule{color:#a78bfa}
|
||||||
|
.layer-title.organism{color:#34d399}
|
||||||
|
.layer-title.template{color:#f0b429}
|
||||||
|
.layer-title.page{color:#f472b6}
|
||||||
|
.layer-list{margin-top:.8rem;display:flex;flex-wrap:wrap;gap:.4rem}
|
||||||
|
.layer-item{
|
||||||
|
padding:.3rem .7rem;background:rgba(255,255,255,.04);
|
||||||
|
border:1px solid var(--border);border-radius:6px;font-size:.78rem;color:var(--text-dim);
|
||||||
|
font-family:'JetBrains Mono',monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── CTA ─── */
|
||||||
|
.cta-section{padding:7rem 2rem;text-align:center}
|
||||||
|
.cta-section h2{font-size:clamp(1.8rem,5vw,3rem);margin-bottom:1rem;line-height:1.15}
|
||||||
|
.cta-section p{color:var(--text-dim);max-width:500px;margin:0 auto 2rem;font-size:1.05rem}
|
||||||
|
.glow-cta{
|
||||||
|
background:linear-gradient(135deg,var(--accent),var(--secondary));
|
||||||
|
box-shadow:0 0 60px var(--accent-glow),0 0 100px var(--secondary-glow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── FOOTER ─── */
|
||||||
|
footer{
|
||||||
|
padding:2.5rem 2rem;text-align:center;color:var(--text-dim);
|
||||||
|
font-size:.78rem;border-top:1px solid var(--border);
|
||||||
|
}
|
||||||
|
footer span{color:var(--accent)}
|
||||||
|
footer a{color:var(--accent);text-decoration:underline}
|
||||||
|
|
||||||
|
/* ─── RESPONSIVE ─── */
|
||||||
|
@media(max-width:640px){
|
||||||
|
.grid{grid-template-columns:1fr}
|
||||||
|
.atomic-grid{grid-template-columns:1fr}
|
||||||
|
.stats{grid-template-columns:repeat(2,1fr)}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- LOADING -->
|
||||||
|
<div id="loading">
|
||||||
|
<div style="font-size:1.2rem;font-weight:900;letter-spacing:.1em">⚡ PULSE 3D</div>
|
||||||
|
<span style="width:240px"></span>
|
||||||
|
<div style="font-size:.7rem;color:var(--text-dim);letter-spacing:.15em">CARREGANDO CENA 3D</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- HERO -->
|
||||||
|
<section class="hero">
|
||||||
|
<div class="badge"><span class="dot"></span> Pulse-3d-landing · Imersivo</div>
|
||||||
|
<h1>
|
||||||
|
<span class="gradient-text">Pulse 3D</span><br>
|
||||||
|
<span style="font-size:.45em;color:var(--text-dim);letter-spacing:.05em;font-weight:600">
|
||||||
|
Landing Page · Design Tokens · Three.js
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
<p>
|
||||||
|
Landing page disruptiva, imersiva e 100% componentizada em 3D dinâmico.
|
||||||
|
Atomic Design · Design Tokens · React Three Fiber · Framer Motion.
|
||||||
|
</p>
|
||||||
|
<div class="cta-row">
|
||||||
|
<button class="btn btn-primary" onclick="window.location.href='#features'">
|
||||||
|
✦ Explorar Funcionalidades
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-ghost" onclick="window.location.href='#atomic'">
|
||||||
|
Ver Componentes →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="scroll-hint"><span>Scroll para explorar</span><div>↓</div></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- FEATURES -->
|
||||||
|
<section class="section" id="features">
|
||||||
|
<span class="section-label">✦ Funcionalidades</span>
|
||||||
|
<h2 class="section-title">6 Pilares do<br><span class="gradient-text">Design System</span></h2>
|
||||||
|
<p class="section-sub">Cada componente desenhado para ser reutilizável, testável e escalável — do átomo ao deploy.</p>
|
||||||
|
<div class="grid">
|
||||||
|
<div class="card">
|
||||||
|
<div class="icon">⚛️</div>
|
||||||
|
<h3>Atomic Design</h3>
|
||||||
|
<p>11 Átomos · 3 Moléculas · 2 Organismos · 2 Templates · 1 Página. Cada nível isolado, testável.</p>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="icon">🎨</div>
|
||||||
|
<h3>Design Tokens</h3>
|
||||||
|
<p>8 domínios: space / font / color / shadow / radius / material3d / camera3d / animation — 100% dinâmicos.</p>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="icon">🎬</div>
|
||||||
|
<h3>Scrollytelling</h3>
|
||||||
|
<p>Scroll move a câmera 3D — experiência cinemática onde o usuário dirige a narrativa.</p>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="icon">✨</div>
|
||||||
|
<h3>Micro-interações</h3>
|
||||||
|
<p>Botões vivos com glow pulsante, hover 3D scale, partículas orgânicas — cada átomo responde.</p>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="icon">♿</div>
|
||||||
|
<h3>WCAG / A11y</h3>
|
||||||
|
<p>Acessibilidade por padrão: semântica, focos visíveis, skip-links, ARIA labels, contraste AAA.</p>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="icon">🚀</div>
|
||||||
|
<h3>Core Web Vitals</h3>
|
||||||
|
<p>Hot path otimizado. Code splitting, LCP < 2.5s, INP < 200ms, CLS < 0.1 — por padrão.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- TECHNOLOGIES -->
|
||||||
|
<section class="tech">
|
||||||
|
<div class="tech-inner">
|
||||||
|
<h2 class="section-title">Tecnologias usadas</h2>
|
||||||
|
<p class="section-sub"style="text-align:center">A stack que alimenta a experiência 3D</p>
|
||||||
|
<div class="tags">
|
||||||
|
<span class="tag">React 18</span>
|
||||||
|
<span class="tag">TypeScript</span>
|
||||||
|
<span class="tag">Vite</span>
|
||||||
|
<span class="tag">Three.js</span>
|
||||||
|
<span class="tag">@react-three/fiber</span>
|
||||||
|
<span class="tag">@react-three/drei</span>
|
||||||
|
<span class="tag">Framer Motion</span>
|
||||||
|
<span class="tag">S.O.L.I.D.</span>
|
||||||
|
<span class="tag">Atomic Design</span>
|
||||||
|
<span class="tag">CSS Custom Properties</span>
|
||||||
|
<span class="tag">WCAG 2.1 AA</span>
|
||||||
|
<span class="tag">Core Web Vitals</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ATOMIC SHOWCASE -->
|
||||||
|
<section class="section atomic" id="atomic">
|
||||||
|
<span class="section-label">✦ Atomic Design</span>
|
||||||
|
<h2 class="section-title">De átomos a<br><span class="gradient-text">organismos vivos</span></h2>
|
||||||
|
<p class="section-sub">A hierarquia de componentes em ação — cada nível herda os tokens dos níveis abaixo.</p>
|
||||||
|
<div class="atomic-grid">
|
||||||
|
<div class="layer-card">
|
||||||
|
<div class="layer-title atom">⚛️ Atoms — 11</div>
|
||||||
|
<div class="layer-list">
|
||||||
|
<span class="layer-item">Button</span><span class="layer-item">Badge</span>
|
||||||
|
<span class="layer-item">Card</span><span class="layer-item">GradientText</span>
|
||||||
|
<span class="layer-item">FloatingText</span><span class="layer-item">LightGlow</span>
|
||||||
|
<span class="layer-item">FloatingMesh</span><span class="layer-item">ParticleField</span>
|
||||||
|
<span class="layer-item">ThemeToggle</span><span class="layer-item">Divider</span>
|
||||||
|
<span class="layer-item">Typography</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="layer-card">
|
||||||
|
<div class="layer-title molecule">🔗 Molecules — 3</div>
|
||||||
|
<div class="layer-list">
|
||||||
|
<span class="layer-item">FloatingMesh</span><span class="layer-item">ParticleField</span>
|
||||||
|
<span class="layer-item">FeatureCard3d</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="layer-card">
|
||||||
|
<div class="layer-title organism">🦠 Organisms — 2</div>
|
||||||
|
<div class="layer-list">
|
||||||
|
<span class="layer-item">HeroScene3d</span><span class="layer-item">FeaturesScene3d</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="layer-card">
|
||||||
|
<div class="layer-title template">📐 Templates — 2</div>
|
||||||
|
<div class="layer-list">
|
||||||
|
<span class="layer-item">SceneCanvas</span><span class="layer-item">ThreePage</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="layer-card">
|
||||||
|
<div class="layer-title page">📄 Page — 1</div>
|
||||||
|
<div class="layer-list">
|
||||||
|
<span class="layer-item">App.tsx</span>
|
||||||
|
<span class="layer-item">Hero + Features + About + CTA</span>
|
||||||
|
<span class="layer-item">~ 500 lines</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- STATS -->
|
||||||
|
<div style="padding:6rem 2rem;max-width:1100px;margin:0 auto">
|
||||||
|
<div class="grid" style="grid-template-columns:repeat(auto-fit,minmax(180px,1fr))">
|
||||||
|
<div class="card" style="text-align:center;padding:2rem">
|
||||||
|
<div style="font-size:2.5rem;font-weight:900;color:var(--accent)">11</div>
|
||||||
|
<div style="color:var(--text-dim);margin-top:.3rem">Átomos</div>
|
||||||
|
</div>
|
||||||
|
<div class="card" style="text-align:center;padding:2rem">
|
||||||
|
<div style="font-size:2.5rem;font-weight:900;color:#a78bfa">8</div>
|
||||||
|
<div style="color:var(--text-dim);margin-top:.3rem">Domínios de Tokens</div>
|
||||||
|
</div>
|
||||||
|
<div class="card" style="text-align:center;padding:2rem">
|
||||||
|
<div style="font-size:2.5rem;font-weight:900;color:#34d399">3D</div>
|
||||||
|
<div style="color:var(--text-dim);margin-top:.3rem">Cena Interativa</div>
|
||||||
|
</div>
|
||||||
|
<div class="card" style="text-align:center;padding:2rem">
|
||||||
|
<div style="font-size:2.5rem;font-weight:900;color:#f0b429">WCAG</div>
|
||||||
|
<div style="color:var(--text-dim);margin-top:.3rem">AAA Acessível</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CTA -->
|
||||||
|
<section class="cta-section" id="cta">
|
||||||
|
<h2>Transforme seu produto em<br>
|
||||||
|
<span class="gradient-text">uma experiência 3D</span>
|
||||||
|
</h2>
|
||||||
|
<p>Do primeiro átomo ao deploy em produção. Design systems vivos que escalam com seu time.</p>
|
||||||
|
<div class="cta-row">
|
||||||
|
<button class="btn btn-primary glow-cta" onclick="window.location.href='#features'">
|
||||||
|
✦ Ver Funcionalidades
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-ghost">
|
||||||
|
Ler Documentação →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<p>⚡ <span>Pulse 3D Landing</span> · test.octal.tec.br · Built with Three.js + React Fiber + Framer Motion · <span style="color:#64748b">WCAG AAA · 2026</span></p>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<!-- PURO CSS SCROLL-DRIVEN 3D HINT (sem JS) ─────────────────────────── -->
|
||||||
|
<script>
|
||||||
|
// Remove loading quando DOM pronto
|
||||||
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
const el = document.getElementById('loading');
|
||||||
|
if (el) el.style.transition = 'opacity .5s', el.style.opacity = '0';
|
||||||
|
setTimeout(() => el?.remove(), 600);
|
||||||
|
}, 1200);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Console branding
|
||||||
|
console.log('%c⚡ PULSE 3D',`
|
||||||
|
font-size:20px;font-weight:900;
|
||||||
|
background:linear-gradient(90deg,#2563eb,#7c3aed);
|
||||||
|
-webkit-background-clip:text;
|
||||||
|
-webkit-text-fill-color:transparent;
|
||||||
|
padding:4px 0;
|
||||||
|
`);
|
||||||
|
console.log('%cAtomic Design + Three.js + Design Tokens','color:#60a5fa;font-size:13px');
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom/client'
|
||||||
|
import { ThreePage } from './components/templates/PageTemplate'
|
||||||
|
import { HeroScene3d } from './components/organisms'
|
||||||
|
import { FeaturesScene3d } from './components/organisms'
|
||||||
|
import { Badge, Button, GradientText, css, ThemeToggle } from './components/atoms'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { tokens } from './systems/tokens'
|
||||||
|
|
||||||
|
// ─── WIREFRAMES → implementação direta ────────────────────────────────
|
||||||
|
/** Wireframe Hero:
|
||||||
|
* overlay 2D sobre cena 3D — título GradientText + CTA bar
|
||||||
|
* Scroll move a câmera no HeroScene3d
|
||||||
|
*/
|
||||||
|
function HeroOverlay() {
|
||||||
|
return (
|
||||||
|
<section style={{ minHeight:'100vh', display:'flex', flexDirection:'column', justifyContent:'center', padding:tokens.space[12], position:'relative', zIndex:20, pointerEvents:'none' }}>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity:0, y:30 }}
|
||||||
|
animate={{ opacity:1, y:0 }}
|
||||||
|
transition={{ duration:1, ease:[0.16,1,0.3,1] }}
|
||||||
|
>
|
||||||
|
<Badge variant="accent" style={{ marginBottom:tokens.space[3] }}>
|
||||||
|
✦ Nova experiência 3D
|
||||||
|
</Badge>
|
||||||
|
|
||||||
|
<GradientText
|
||||||
|
from="#60a5fa" to="#a78bfa"
|
||||||
|
size="clamp(2.5rem,6vw,4.5rem)"
|
||||||
|
style={{ display:'block', marginBottom:tokens.space[4] }}
|
||||||
|
>
|
||||||
|
Pulse 3D
|
||||||
|
</GradientText>
|
||||||
|
|
||||||
|
<p style={{ ...css.body(), fontSize:tokens.font.size.xl, maxWidth:'600px', marginBottom:tokens.space[5] }}>
|
||||||
|
Landing page disruptiva, imersiva e 100% componentizada em 3D dinâmico.
|
||||||
|
Scroll para explorar. Clique para interagir.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style={{ display:'flex', gap:tokens.space[3], flexWrap:'wrap', pointerEvents:'auto' }}>
|
||||||
|
<Badge variant="neutral">Three.js + React Fiber</Badge>
|
||||||
|
<Badge variant="neutral">Framer Motion</Badge>
|
||||||
|
<Badge variant="neutral">Design Tokens</Badge>
|
||||||
|
<Badge variant="neutral">Atomic Design</Badge>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Indicador de scroll */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity:0 }}
|
||||||
|
animate={{ opacity:1 }}
|
||||||
|
transition={{ delay:2 }}
|
||||||
|
style={{ position:'absolute', bottom:tokens.space[6], left:'50%', transform:'translateX(-50%)', color:tokens.color.gray500, fontSize:tokens.font.size.sm, display:'flex', flexDirection:'column', alignItems:'center', gap:tokens.space[2] }}
|
||||||
|
>
|
||||||
|
<span style={{ letterSpacing:'0.2em', textTransform:'uppercase', fontSize:tokens.font.size.xs }}>Scroll</span>
|
||||||
|
<motion.div animate={{ y:[0,8,0] }} transition={{ repeat:Infinity, duration:1.5 }}>↓</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Wireframe Features:
|
||||||
|
* headline + grid de FeatureCards + cena 3D orbitando no fundo
|
||||||
|
*/
|
||||||
|
function FeaturesSection() {
|
||||||
|
return (
|
||||||
|
<section id="features" style={{ minHeight:'150vh', padding:tokens.space[12], display:'flex', flexDirection:'column', gap:tokens.space[8] }}>
|
||||||
|
<div style={{ maxWidth:'640px' }}>
|
||||||
|
<Badge variant="secondary">✦ Funcionalidades</Badge>
|
||||||
|
<h2 style={{ ...css.heading(), fontSize:tokens.font.size[4xl], margin:`${tokens.space[3]} 0` }}>
|
||||||
|
6 Pilares do Design System
|
||||||
|
</h2>
|
||||||
|
<p style={css.body()}>
|
||||||
|
Cada componente foi desenhado para ser reutilizável, testável e escalável.
|
||||||
|
Abaixo, os átomos se organizam em organismos vivos em 3D.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display:'grid', gridTemplateColumns:'repeat(auto-fill, minmax(360px, 1fr))', gap:tokens.space[4], maxWidth:'1100px' }}>
|
||||||
|
{[
|
||||||
|
['Atomic Design','Componentes isolados,fullstack, testáveis em nível atômico.'],
|
||||||
|
['Design Tokens','Cores/ESPAÇO/tipografia/material3D 100% dinâmicos via CSS vars.'],
|
||||||
|
['Scrollytelling','Scroll move a câmera — experiência cinematográfica guiada.'],
|
||||||
|
['Micro-interações','Hover 3D, glow pulsante, física orgânica em todos os botões.'],
|
||||||
|
['WCAG / A11y','Semântica completa, focos visíveis, skip-links, ARIA labels.'],
|
||||||
|
['Core Web Vitals','LCP/INP/CLS otimizados desde o átomo.'],
|
||||||
|
].map(([title, desc]) => (
|
||||||
|
<motion.div
|
||||||
|
key={title}
|
||||||
|
initial={{ opacity:0, y:20 }}
|
||||||
|
whileInView ={{ opacity:1, y:0 }}
|
||||||
|
viewport={{ once:true, margin:'-80px' }}
|
||||||
|
transition ={{ duration:0.5 }}
|
||||||
|
>
|
||||||
|
<div style={{ padding:tokens.space[5], height:'100%' }}>
|
||||||
|
<h3 style={{ fontSize:tokens.font.size.xl, fontWeight:tokens.font.weight.semibd, color:tokens.color.white, marginBottom:tokens.space[2] }}>{title}</h3>
|
||||||
|
<p style={css.body({ fontSize:tokens.font.size.sm })}>{desc}</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style={{ color:tokens.color.gray500, fontSize:tokens.font.size.sm, textAlign:'center' }}>
|
||||||
|
↑ Objetos 3D flutuando acima — objeto orbitando a cena continuamente
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Wireframe About:
|
||||||
|
* Texto de esquerda + objeto 3D de direita
|
||||||
|
*/
|
||||||
|
function AboutSection() {
|
||||||
|
return (
|
||||||
|
<section style={{ minHeight:'100vh', display:'flex', alignItems:'center', gap:tokens.space[10], padding:tokens.space[12] }}>
|
||||||
|
<div style={{ maxWidth:'600px' }}>
|
||||||
|
<Badge variant="accent">✦ Filosofia</Badge>
|
||||||
|
<h2 style={{ ...css.heading(), fontSize:tokens.font.size[3xl], margin:`${tokens.space[3]} 0` }}>
|
||||||
|
Código como<br/><GradientText from="#2563eb" to="#7c3aed">experiência</GradientText>
|
||||||
|
</h2>
|
||||||
|
<p style={css.body()}>
|
||||||
|
Cada pixel é um átomo. Cada interação, uma molécula. Cada página, um organismo vivo.
|
||||||
|
O Design System é o DNA que garante consistência e escala — do componente ao portfólio.
|
||||||
|
</p>
|
||||||
|
<ul style={{ listStyle:'none', padding:0, display:'flex', flexDirection:'column', gap:tokens.space[2], marginTop:tokens.space[4] }}>
|
||||||
|
{['S.O.L.I.D. em todas as camadas','Atomic Design do átomo ao deploy','Design Tokens dinâmicos (dark/light/3D)','A11y-first, performance-first'].map(t => (
|
||||||
|
<li key={t} style={{ display:'flex', gap:tokens.space[2], alignItems:'center', color:tokens.color.gray300, fontSize:tokens.font.size.sm }}>
|
||||||
|
<span style={{ color:tokens.color.accent }}>◆</span> {t}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Wireframe CTA + Footer */
|
||||||
|
function CtaSection() {
|
||||||
|
return (
|
||||||
|
<section style={{ minHeight:'70vh', display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center', textAlign:'center', padding:tokens.space[12], gap:tokens.space[6] }}>
|
||||||
|
<Badge variant="neutral">✦ Vamos criar juntos?</Badge>
|
||||||
|
<h2 style={{ ...css.heading(), fontSize:tokens.font.size[4xl] }}>
|
||||||
|
Pronto para<br/><GradientText from="#f97316" to="#ef4444">revolucionar</GradientText>?
|
||||||
|
</h2>
|
||||||
|
<p style={{ ...css.body(), textAlign:'center', maxWidth:'480px' }}>
|
||||||
|
Do primeiro átomo ao deploy em produção.
|
||||||
|
Um design system vivo para produtos que duram.
|
||||||
|
</p>
|
||||||
|
<div style={{ display:'flex', gap:tokens.space[3], flexWrap:'wrap', justifyContent:'center' }}>
|
||||||
|
<Button variant="primary" size="lg" onClick={() => alert('🚀 Começar!')}>
|
||||||
|
Começar Agora →
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="lg">
|
||||||
|
Ver Documentação
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<footer style={{ marginTop:tokens.space[10], color:tokens.color.gray500, fontSize:tokens.font.size.xs, letterSpacing:'0.05em' }}>
|
||||||
|
<p>© 2026 Pulse 3D · Build with ♥ + Three.js · WCAG AAA</p>
|
||||||
|
</footer>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── APP ─────────────────────────────────────────────────────────────
|
||||||
|
export default function App() {
|
||||||
|
const [loaded, setLoaded] = useState(false)
|
||||||
|
|
||||||
|
React.useEffect(() => { setLoaded(true) }, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ThemeToggle />
|
||||||
|
<ThreePage
|
||||||
|
orbitControls
|
||||||
|
scrollPages={7}
|
||||||
|
canvasChildren={
|
||||||
|
<>
|
||||||
|
<HeroScene3d />
|
||||||
|
<FeaturesScene3d />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
overlayChildren={
|
||||||
|
<>
|
||||||
|
<HeroOverlay />
|
||||||
|
<FeaturesSection />
|
||||||
|
<AboutSection />
|
||||||
|
<CtaSection />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
/** Átomo: Badge
|
||||||
|
* Etiqueta pequena — label de feature, versão ou categoria.
|
||||||
|
*/
|
||||||
|
import { CSSProperties } from 'react'
|
||||||
|
import { tokens } from '../../systems/tokens'
|
||||||
|
|
||||||
|
const base: CSSProperties = {
|
||||||
|
display : 'inline-flex',
|
||||||
|
alignItems : 'center',
|
||||||
|
padding : `${tokens.space[1]} ${tokens.space[3]}`,
|
||||||
|
fontSize : tokens.font.size.xs,
|
||||||
|
fontFamily : tokens.font.family.mono,
|
||||||
|
fontWeight : tokens.font.weight.semibd,
|
||||||
|
letterSpacing: '0.08em',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
borderRadius: tokens.radius.full,
|
||||||
|
border : `1px solid`,
|
||||||
|
transition : `all ${tokens.animation.quick}s`,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Badge({
|
||||||
|
children,
|
||||||
|
variant = 'accent',
|
||||||
|
icon,
|
||||||
|
style,
|
||||||
|
}: {
|
||||||
|
children : React.ReactNode
|
||||||
|
variant ?: 'accent' | 'secondary' | 'neutral' | 'success'
|
||||||
|
icon ?: React.ReactNode
|
||||||
|
style ?: CSSProperties
|
||||||
|
}) {
|
||||||
|
const styles: Record<string, CSSProperties> = {
|
||||||
|
accent : { color: tokens.color.accent, borderColor: tokens.color.accent + '60', background: tokens.color.accentMuted },
|
||||||
|
secondary: { color: tokens.color.secondary, borderColor: tokens.color.secondary + '60', background: tokens.color.secondaryMuted },
|
||||||
|
neutral : { color: tokens.color.gray400, borderColor: tokens.color.gray700, background: 'transparent' },
|
||||||
|
success : { color: tokens.color.success, borderColor: tokens.color.success + '60', background: 'rgba(34,197,94,.1)' },
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<span style={{...base, ...styles[variant], ...style }}>
|
||||||
|
{icon && <span style={{ marginRight: 4 }}>{icon}</span>}
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
/** Átomo DOM: Button
|
||||||
|
* Botão com glow dinâmico, hover 3D-like (scale + glow).
|
||||||
|
* Usa CSS variables do design token — funciona em modo claro/escuro.
|
||||||
|
*/
|
||||||
|
import { CSSProperties } from 'react'
|
||||||
|
import { tokens } from '../../systems/tokens'
|
||||||
|
|
||||||
|
const baseStyle: CSSProperties = {
|
||||||
|
display : 'inline-flex',
|
||||||
|
alignItems : 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap : tokens.space[2],
|
||||||
|
padding : `${tokens.space[3]} ${tokens.space[5]}`,
|
||||||
|
borderRadius: tokens.radius.full,
|
||||||
|
fontFamily : tokens.font.family.display,
|
||||||
|
fontWeight : tokens.font.weight.semibd,
|
||||||
|
fontSize : tokens.font.size.base,
|
||||||
|
lineHeight : tokens.font.line.normal,
|
||||||
|
cursor : 'pointer',
|
||||||
|
border : 'none',
|
||||||
|
outline : 'none',
|
||||||
|
transition : `transform ${tokens.animation.quick}s ${tokens.animation.easeOut},
|
||||||
|
box-shadow ${tokens.animation.quick}s ${tokens.animation.easeOut},
|
||||||
|
background ${tokens.animation.instant}s`,
|
||||||
|
transform : 'perspective(400px) translateZ(0)',
|
||||||
|
position : 'relative',
|
||||||
|
overflow : 'hidden',
|
||||||
|
} as CSSProperties
|
||||||
|
|
||||||
|
export function Button({
|
||||||
|
children,
|
||||||
|
variant: _v = 'primary',
|
||||||
|
size = 'md',
|
||||||
|
disabled = false,
|
||||||
|
onClick,
|
||||||
|
style,
|
||||||
|
className = '',
|
||||||
|
type = 'button',
|
||||||
|
'aria-label': ariaLabel,
|
||||||
|
}: {
|
||||||
|
children : React.ReactNode
|
||||||
|
variant ?: 'primary' | 'secondary' | 'ghost'
|
||||||
|
size ?: 'sm' | 'md' | 'lg'
|
||||||
|
disabled ?: boolean
|
||||||
|
onClick ?: () => void
|
||||||
|
style ?: CSSProperties
|
||||||
|
className?: string
|
||||||
|
type ?: 'button' | 'submit'
|
||||||
|
'aria-label'?: string
|
||||||
|
}) {
|
||||||
|
const sizeMap: Record<string, CSSProperties> = {
|
||||||
|
sm: { padding: `${tokens.space[2]} ${tokens.space[3]}`, fontSize: tokens.font.size.sm },
|
||||||
|
md: { padding: `${tokens.space[3]} ${tokens.space[5]}`, fontSize: tokens.font.size.base },
|
||||||
|
lg: { padding: `${tokens.space[4]} ${tokens.space[6]}`, fontSize: tokens.font.size.lg },
|
||||||
|
}
|
||||||
|
|
||||||
|
const variants: Record<string, CSSProperties> = {
|
||||||
|
primary: {
|
||||||
|
background : tokens.color.accent,
|
||||||
|
color : '#ffffff',
|
||||||
|
boxShadow : tokens.shadow.glow,
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
background : tokens.color.secondaryMuted,
|
||||||
|
color : tokens.color.secondary,
|
||||||
|
boxShadow : 'none',
|
||||||
|
},
|
||||||
|
ghost: {
|
||||||
|
background : 'transparent',
|
||||||
|
color : tokens.color.gray300,
|
||||||
|
border : `1px solid ${tokens.color.gray600}`,
|
||||||
|
boxShadow : 'none',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type={type}
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={onClick}
|
||||||
|
style={{
|
||||||
|
...baseStyle,
|
||||||
|
...variants[_v],
|
||||||
|
...sizeMap[size],
|
||||||
|
opacity: disabled ? 0.4 : 1,
|
||||||
|
cursor : disabled ? 'not-allowed' : 'pointer',
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
className={`btn-glitch ${className}`}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { CSSProperties, ReactNode } from 'react'
|
||||||
|
import { tokens } from '../../systems/tokens'
|
||||||
|
import { createPortal } from 'react-dom'
|
||||||
|
|
||||||
|
const base: CSSProperties = {
|
||||||
|
background : 'rgba(15,17,23,.78)',
|
||||||
|
backdropFilter: 'blur(16px)',
|
||||||
|
WebkitBackdropFilter: 'blur(16px)',
|
||||||
|
border : `1px solid rgba(51,65,85,.6)`,
|
||||||
|
borderRadius: tokens.radius.lg,
|
||||||
|
boxShadow : tokens.shadow.lg,
|
||||||
|
transition : `border-color ${tokens.animation.quick}s,
|
||||||
|
box-shadow ${tokens.animation.quick}s`,
|
||||||
|
overflow : 'hidden',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Card({
|
||||||
|
children, style, elevated = false, glow = false,
|
||||||
|
}: {
|
||||||
|
children : ReactNode
|
||||||
|
style ?: CSSProperties
|
||||||
|
elevated ?: boolean // corrige badge 1000000000
|
||||||
|
glow ?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
...base,
|
||||||
|
...(elevated ? { borderColor:'rgba(96,165,250,.3)', boxShadow:'0 0 40px rgba(37,99,235,.15)' } : {}),
|
||||||
|
...(glow ? { boxShadow: '0 0 30px rgba(37,99,235,.20), inset 0 0 20px rgba(37,99,235,.05)' } : {}),
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { CSSProperties } from 'react'
|
||||||
|
import { tokens } from '../../systems/tokens'
|
||||||
|
|
||||||
|
export function Divider({ vertical=false, style }: { vertical?: boolean; style?: CSSProperties }) {
|
||||||
|
return (
|
||||||
|
<hr style={{
|
||||||
|
border : 'none',
|
||||||
|
height : vertical ? '1px' : 'auto',
|
||||||
|
width : vertical ? '1px' : '100%',
|
||||||
|
flex : vertical ? 'none' : 1,
|
||||||
|
background: 'linear-gradient(90deg, transparent, rgba(37,99,235,.2), transparent)',
|
||||||
|
margin : 0,
|
||||||
|
...style,
|
||||||
|
}} />
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/** Átomo: FloatingText
|
||||||
|
* Texto flutuante em 3D — aparece no mundo Three.js.
|
||||||
|
* Size units: unidades 3D (não CSS px).
|
||||||
|
*/
|
||||||
|
import { Text } from '@react-three/drei'
|
||||||
|
import { useRef } from 'react'
|
||||||
|
import * as THREE from 'three'
|
||||||
|
|
||||||
|
export function FloatingText({
|
||||||
|
children,
|
||||||
|
position = [0, 0, 0] as [number, number, number],
|
||||||
|
size = 0.5,
|
||||||
|
color = '#e4e4e7',
|
||||||
|
maxWidth = 8,
|
||||||
|
letterSpacing = 0.05,
|
||||||
|
outlineWidth = 0.002,
|
||||||
|
outlineColor = '#2563eb',
|
||||||
|
font = '/fonts/Inter-Bold.woff2',
|
||||||
|
anchorX = 'center',
|
||||||
|
anchorY = 'middle',
|
||||||
|
rotation = [0, 0, 0] as [number, number, number],
|
||||||
|
}: {
|
||||||
|
children: string
|
||||||
|
position?: [number, number, number]
|
||||||
|
size?: number
|
||||||
|
color?: string
|
||||||
|
maxWidth?: number
|
||||||
|
letterSpacing?: number
|
||||||
|
outlineWidth?: number
|
||||||
|
outlineColor?: string
|
||||||
|
font?: string
|
||||||
|
anchorX?: 'left' | 'center' | 'right'
|
||||||
|
anchorY?: 'top' | 'top-baseline' | 'middle' | 'bottom-baseline' | 'bottom'
|
||||||
|
rotation?: [number, number, number]
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Text
|
||||||
|
position={position}
|
||||||
|
fontSize={size}
|
||||||
|
color={color}
|
||||||
|
maxWidth={maxWidth}
|
||||||
|
letterSpacing={letterSpacing}
|
||||||
|
outlineWidth={outlineWidth}
|
||||||
|
outlineColor={outlineColor}
|
||||||
|
font={font}
|
||||||
|
anchorX={anchorX}
|
||||||
|
anchorY={anchorY}
|
||||||
|
rotation={rotation}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { ReactNode, CSSProperties } from 'react'
|
||||||
|
import { tokens } from '../../systems/tokens'
|
||||||
|
|
||||||
|
export function GradientText({ children, from='#60a5fa', to='#a78bfa', size='3.25rem', weight=700, style }: {
|
||||||
|
children: ReactNode
|
||||||
|
from ?: string
|
||||||
|
to ?: string
|
||||||
|
size ?: string
|
||||||
|
weight ?: number
|
||||||
|
style ?: CSSProperties
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<span style={{
|
||||||
|
background : `linear-gradient(135deg, ${from} 0%, ${to} 100%)`,
|
||||||
|
WebkitBackgroundClip: 'text',
|
||||||
|
WebkitTextFillColor : 'transparent',
|
||||||
|
backgroundClip: 'text',
|
||||||
|
fontSize : size,
|
||||||
|
fontWeight : weight,
|
||||||
|
letterSpacing : '-0.02em',
|
||||||
|
lineHeight : '1.1',
|
||||||
|
...style,
|
||||||
|
}}>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/** Átomo: LightGlow
|
||||||
|
* Efeito de glow que rodeia elementos 3D ou DOM.
|
||||||
|
* A intensidade é = intensidade da luz no ponto3d.
|
||||||
|
*/
|
||||||
|
import { useThree } from '@react-three/fiber'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
import * as THREE from 'three'
|
||||||
|
|
||||||
|
export function LightGlow({
|
||||||
|
color = '#2563eb',
|
||||||
|
intensity = 1.5,
|
||||||
|
distance = 12,
|
||||||
|
position = [-4, 3, 2] as [number, number, number],
|
||||||
|
decay = 2,
|
||||||
|
enabled = true,
|
||||||
|
}: {
|
||||||
|
color?: string | THREE.Color
|
||||||
|
intensity?: number
|
||||||
|
distance?: number
|
||||||
|
position?: [number, number, number]
|
||||||
|
decay?: number
|
||||||
|
enabled?: boolean
|
||||||
|
}) {
|
||||||
|
const { scene } = useThree()
|
||||||
|
useEffect(() => {
|
||||||
|
if (!enabled) return
|
||||||
|
const light = new THREE.PointLight(color, intensity, distance, decay)
|
||||||
|
light.position.set(...position)
|
||||||
|
scene.add(light)
|
||||||
|
return () => { scene.remove(light); light.dispose() }
|
||||||
|
}, [scene, color, intensity, distance, position, decay, enabled])
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { useState, CSSProperties } from 'react'
|
||||||
|
import { tokens } from '../../systems/tokens'
|
||||||
|
|
||||||
|
export function ThemeToggle() {
|
||||||
|
const [dark, setDark] = useState(true)
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={() => { setDark(!dark); document.body.style.setProperty('--theme-mode', dark ? 'light' : 'dark') }}
|
||||||
|
style={{
|
||||||
|
background: dark
|
||||||
|
? 'linear-gradient(135deg,#2563eb,#7c3aed)'
|
||||||
|
: 'linear-gradient(135deg,#fbbf24,#f97316)',
|
||||||
|
border:'none', borderRadius: tokens.radius.full,
|
||||||
|
padding: `${tokens.space[2]} ${tokens.space[3]}`,
|
||||||
|
cursor:'pointer', fontSize: tokens.font.size.sm,
|
||||||
|
color:'#fff', fontFamily: tokens.font.family.display,
|
||||||
|
boxShadow: tokens.shadow.glow,
|
||||||
|
transition: `background ${tokens.animation.quick}s`,
|
||||||
|
position:'fixed', top: tokens.space[4], right: tokens.space[4],
|
||||||
|
zIndex: 9999,
|
||||||
|
} as CSSProperties}
|
||||||
|
aria-label="Alternar tema dia/noite 3D"
|
||||||
|
>
|
||||||
|
{dark ? '🌙 Noite' : '☀️ Dia'}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import { FC, ReactNode } from 'react'
|
||||||
|
import { tokens } from '../../systems/tokens'
|
||||||
|
|
||||||
|
export type IconType = 'hero' | 'features' | 'about' | 'testimonials' | 'cta'
|
||||||
|
|
||||||
|
export const icons: Record<IconType, { path: string; label: string }> = {
|
||||||
|
hero: { path: 'M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5', label: 'Hero' },
|
||||||
|
features: { path: 'M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z', label: 'Features' },
|
||||||
|
about: { path: 'M12 2a10 10 0 100 20 10 10 0 000-20zM12 6v6l4 2', label: 'About' },
|
||||||
|
testimonials:{ path: 'M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2v10z', label: 'Testimonials'},
|
||||||
|
cta: { path: 'M21 11.5a8.38 8.38 0 01-.9 3.8 8.5 8.5 0 01-7.6 4.7 8.38 8.38 0 01-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 01-.9-3.8 8.5 8.5 0 014.7-7.6 8.38 8.38 0 013.8-.9h.5a8.48 8.48 0 018 8v.5z', label: 'CTA' },
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Icon({ type, size=28, color=tokens.color.accent }: { type: IconType; size?: number; color?: string }) {
|
||||||
|
const i = icons[type]
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width={size} height={size} viewBox="0 0 24 24" fill="none"
|
||||||
|
stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
focusable="false"
|
||||||
|
>
|
||||||
|
<path d={i.path} />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** OverlayFullWidth helper */
|
||||||
|
export function OverlayFullWidth({ children, style }: { children: ReactNode; style?: React.CSSProperties }) {
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
position: 'relative', zIndex: 10, pointerEvents: 'none',
|
||||||
|
...style,
|
||||||
|
}}>
|
||||||
|
{React.Children.map(children, child =>
|
||||||
|
React.isValidElement(child)
|
||||||
|
? React.cloneElement(child as any, { style: { ...(child.props.style||{}), pointerEvents: 'auto' } })
|
||||||
|
: child
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
/** Inline style helpers — atoms em React */
|
||||||
|
export const css = {
|
||||||
|
section: (overrides?: React.CSSProperties): React.CSSProperties => ({
|
||||||
|
position : 'relative',
|
||||||
|
minHeight : '100vh',
|
||||||
|
padding : `${tokens.space[10]} ${tokens.space[4]}`,
|
||||||
|
display : 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
gap : tokens.space[8],
|
||||||
|
overflow : 'hidden',
|
||||||
|
maxWidth : '1440px',
|
||||||
|
margin : '0 auto',
|
||||||
|
...overrides,
|
||||||
|
}),
|
||||||
|
heading: (overrides?: React.CSSProperties): React.CSSProperties => ({
|
||||||
|
fontFamily: tokens.font.family.display,
|
||||||
|
fontWeight: tokens.font.weight.black,
|
||||||
|
fontSize : tokens.font.size[6xl],
|
||||||
|
lineHeight: tokens.font.line.tight,
|
||||||
|
letterSpacing: '-0.03em',
|
||||||
|
color : tokens.color.white,
|
||||||
|
...overrides,
|
||||||
|
}),
|
||||||
|
subheading: (overrides?: React.CSSProperties): React.CSSProperties => ({
|
||||||
|
fontFamily: tokens.font.family.display,
|
||||||
|
fontWeight: tokens.font.weight.semibd,
|
||||||
|
fontSize : tokens.font.size[3xl],
|
||||||
|
color : tokens.color.gray100,
|
||||||
|
lineHeight: tokens.font.line.tight,
|
||||||
|
...overrides,
|
||||||
|
}),
|
||||||
|
body: (overrides?: React.CSSProperties): React.CSSProperties => ({
|
||||||
|
fontFamily: tokens.font.family.body,
|
||||||
|
fontWeight: tokens.font.weight.normal,
|
||||||
|
fontSize : tokens.font.size.lg,
|
||||||
|
color : tokens.color.gray300,
|
||||||
|
lineHeight: tokens.font.line.normal,
|
||||||
|
maxWidth : '540px',
|
||||||
|
...overrides,
|
||||||
|
}),
|
||||||
|
badge: (overrides?: React.CSSProperties): React.CSSProperties => ({
|
||||||
|
display : 'inline-flex',
|
||||||
|
alignItems : 'center',
|
||||||
|
gap : tokens.space[2],
|
||||||
|
padding : `${tokens.space[1]} ${tokens.space[3]}`,
|
||||||
|
border : `1px solid ${tokens.color.accent}60`,
|
||||||
|
borderRadius: tokens.radius.full,
|
||||||
|
background : `${tokens.color.accent}14`,
|
||||||
|
color : tokens.color.accentLight,
|
||||||
|
fontSize : tokens.font.size.xs,
|
||||||
|
fontFamily : tokens.font.family.mono,
|
||||||
|
fontWeight : tokens.font.weight.semibd,
|
||||||
|
letterSpacing: '0.1em',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
backdropFilter: 'blur(8px)',
|
||||||
|
...overrides,
|
||||||
|
}),
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export { Badge } from './Badge'
|
||||||
|
export { Button } from './Button'
|
||||||
|
export { GradientText } from './GradientText'
|
||||||
|
export { Card } from './Card'
|
||||||
|
export { Divider } from './Divider'
|
||||||
|
export { ThemeToggle } from './ThemeToggle'
|
||||||
|
export type { IconType, icons } from './Typography'
|
||||||
|
export { Icon, OverlayFullWidth, css } from './Typography'
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { FloatingMesh } from '../atoms/FloatingMesh'
|
||||||
|
import { Badge, Button, GradientText, Card, css } from '../atoms'
|
||||||
|
import { tokens } from '../../systems/tokens'
|
||||||
|
|
||||||
|
/** Molécula: FeatureCard3d
|
||||||
|
* Card feature com objeto 3D ao lado.
|
||||||
|
*/
|
||||||
|
export function FeatureCard3d({
|
||||||
|
badge,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
shape='box',
|
||||||
|
accentColor=tokens.color.accent,
|
||||||
|
}: {
|
||||||
|
badge : string
|
||||||
|
title : string
|
||||||
|
description: string
|
||||||
|
shape ?: 'box' | 'sphere'
|
||||||
|
accentColor?: string
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Card elevated glow style={{ padding: tokens.space[5], display:'flex', gap: tokens.space[5], alignItems:'center' }}>
|
||||||
|
<div style={{ flexShrink:0 }}>
|
||||||
|
<FloatingMesh
|
||||||
|
geometry={shape}
|
||||||
|
size={1.6}
|
||||||
|
color={accentColor}
|
||||||
|
emissive={accentColor+'60'}
|
||||||
|
floatSpeed={0.7}
|
||||||
|
floatAmp={0.2}
|
||||||
|
spinSpeed={0.12}
|
||||||
|
showGlow
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Badge variant="accent">{badge}</Badge>
|
||||||
|
<h3 style={{ ...css.subheading(), fontSize:tokens.font.size.xl, margin:`${tokens.space[2]} 0 ${tokens.space[1]}`, color: tokens.color.white }}>
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
<p style={css.body({ fontSize:tokens.font.size.sm })}>{description}</p>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
export { FeatureCard3d } from './FeatureCard3d'
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
/** Molécula: FloatingMesh
|
||||||
|
* Geometria que flutua, rotaciona e reage ao hover.
|
||||||
|
* Combina: Mesh (geometria 3D) + Material + núcleo de luz.
|
||||||
|
*/
|
||||||
|
import { useRef, useState } from 'react'
|
||||||
|
import { Mesh, MeshStandardMaterial } from 'three'
|
||||||
|
import { useFrame } from '@react-three/fiber'
|
||||||
|
import { RoundedBox, Sphere, Torus, Text } from '@react-three/drei'
|
||||||
|
import * as THREE from 'three'
|
||||||
|
|
||||||
|
export function FloatingMesh({
|
||||||
|
geometry = 'box',
|
||||||
|
size = 1.5,
|
||||||
|
position = [0, 0, 0] as [number, number, number],
|
||||||
|
color = '#2563eb',
|
||||||
|
metalness = 0.75,
|
||||||
|
roughness = 0.08,
|
||||||
|
emissive = '#1e3a8a',
|
||||||
|
emissiveIntensity = 0.3,
|
||||||
|
floatSpeed = 0.6,
|
||||||
|
floatAmp = 0.3,
|
||||||
|
spinSpeed = 0.12,
|
||||||
|
showGlow = true,
|
||||||
|
label,
|
||||||
|
}: {
|
||||||
|
geometry?: 'box' | 'sphere' | 'torus'
|
||||||
|
size?: number
|
||||||
|
position?: [number, number, number]
|
||||||
|
color?: string
|
||||||
|
metalness?: number
|
||||||
|
roughness?: number
|
||||||
|
emissive?: string
|
||||||
|
emissiveIntensity?: number
|
||||||
|
floatSpeed?: number
|
||||||
|
floatAmp?: number
|
||||||
|
spinSpeed?: number
|
||||||
|
showGlow?: boolean
|
||||||
|
label?: string
|
||||||
|
}) {
|
||||||
|
const meshRef = useRef<THREE.Mesh>(null!)
|
||||||
|
const [hovered, setHovered] = useState(false)
|
||||||
|
const t = useRef(0)
|
||||||
|
|
||||||
|
useFrame((_s, dt) => {
|
||||||
|
t.current += dt * floatSpeed
|
||||||
|
if (meshRef.current) {
|
||||||
|
meshRef.current.position.y = position[1] + Math.sin(t.current) * floatAmp
|
||||||
|
meshRef.current.rotation.y += dt * spinSpeed * (hovered ? 2 : 1)
|
||||||
|
meshRef.current.rotation.x += dt * spinSpeed * 0.3
|
||||||
|
// hover scale
|
||||||
|
const target = hovered ? 1.12 : 1.0
|
||||||
|
meshRef.current.scale.lerp(new THREE.Vector3(target, target, target), dt * 6)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const mat = (
|
||||||
|
<meshStandardMaterial
|
||||||
|
color={color}
|
||||||
|
metalness={metalness}
|
||||||
|
roughness={roughness}
|
||||||
|
emissive={emissive}
|
||||||
|
emissiveIntensity={emissiveIntensity + (hovered ? 0.4 : 0)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
let geom: React.ReactNode
|
||||||
|
switch (geometry) {
|
||||||
|
case 'sphere': geom = <Sphere args={[size/2, 32, 32]} />; break
|
||||||
|
case 'torus': geom = <Torus args={[size/2, size/6, 24, 48]} />; break
|
||||||
|
default: geom = <RoundedBox args={[size, size, size]} radius={0.1} smoothness={4} />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group position={position}>
|
||||||
|
<mesh
|
||||||
|
ref={meshRef}
|
||||||
|
onPointerOver={() => setHovered(true)}
|
||||||
|
onPointerOut={() => setHovered(false)}
|
||||||
|
>
|
||||||
|
{geom}
|
||||||
|
{mat}
|
||||||
|
</mesh>
|
||||||
|
{label && (
|
||||||
|
<Text
|
||||||
|
position={[0, size/2 + 0.6, 0]}
|
||||||
|
fontSize={0.22}
|
||||||
|
color="#e4e4e7"
|
||||||
|
anchorX="center"
|
||||||
|
anchorY="middle"
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{showGlow && (
|
||||||
|
<pointLight
|
||||||
|
color={color}
|
||||||
|
intensity={hovered ? 3 : 1.2}
|
||||||
|
distance={8}
|
||||||
|
decay={2}
|
||||||
|
position={[0, 0, 0]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</group>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
/** Molécula: ParticleField
|
||||||
|
* Campo de partículas tipo estrelas/dust — cria profundidade atmosférica.
|
||||||
|
*/
|
||||||
|
import { useMemo } from 'react'
|
||||||
|
import { useFrame } from '@react-three/fiber'
|
||||||
|
import * as THREE from 'three'
|
||||||
|
|
||||||
|
export function ParticleField({
|
||||||
|
count = 2000,
|
||||||
|
spread = 30,
|
||||||
|
color = '#93c5fd',
|
||||||
|
size = 0.04,
|
||||||
|
speed = 0.15,
|
||||||
|
}: {
|
||||||
|
count ?: number
|
||||||
|
spread ?: number
|
||||||
|
color ?: string
|
||||||
|
size ?: number
|
||||||
|
speed ?: number
|
||||||
|
}) {
|
||||||
|
const points = useMemo(() => {
|
||||||
|
const arr = new Float32Array(count * 4)
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const i4 = i * 4
|
||||||
|
arr[i4] = (Math.random() - 0.5) * spread // x
|
||||||
|
arr[i4 + 1] = (Math.random() - 0.5) * spread // y
|
||||||
|
arr[i4 + 2] = (Math.random() - 0.5) * spread // z
|
||||||
|
arr[i4 + 3] = Math.random() // random
|
||||||
|
}
|
||||||
|
return arr
|
||||||
|
}, [count, spread])
|
||||||
|
|
||||||
|
const ref = useRef<THREE.Points>(null!)
|
||||||
|
|
||||||
|
useFrame((_s, dt) => {
|
||||||
|
if (ref.current) {
|
||||||
|
ref.current.rotation.y += dt * speed * 0.02
|
||||||
|
const positions = ref.current.geometry.attributes.position as THREE.BufferAttribute
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const i4 = i * 4
|
||||||
|
positions.array[i4 + 1] += Math.sin(Date.now() * 0.001 + positions.array[i4]) * dt * 0.02
|
||||||
|
}
|
||||||
|
positions.needsUpdate = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<points ref={ref}>
|
||||||
|
<bufferGeometry>
|
||||||
|
<bufferAttribute
|
||||||
|
attach="attributes-position"
|
||||||
|
args={[points, 4]}
|
||||||
|
/>
|
||||||
|
</bufferGeometry>
|
||||||
|
<pointsMaterial
|
||||||
|
color={color}
|
||||||
|
size={size}
|
||||||
|
transparent
|
||||||
|
opacity={0.6}
|
||||||
|
sizeAttenuation
|
||||||
|
depthWrite={false}
|
||||||
|
/>
|
||||||
|
</points>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export { FloatingMesh } from './FloatingMesh'
|
||||||
|
export { ParticleField } from './ParticleField'
|
||||||
|
export { FeatureCard3d } from './FeatureCard3d'
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
/** Organismo: FeaturesScene3d
|
||||||
|
* Seção de funcionalidades — objetos 3D em grid flutuante orbitais.
|
||||||
|
*/
|
||||||
|
import { useRef } from 'react'
|
||||||
|
import { useFrame } from '@react-three/fiber'
|
||||||
|
import { Text, Float } from '@react-three/drei'
|
||||||
|
import * as THREE from 'three'
|
||||||
|
|
||||||
|
const FEATURES = [
|
||||||
|
{ id:1, title:'Atomic Design', desc:'Componentes isolados, reutilizáveis e testáveis.', color:'#2563eb' },
|
||||||
|
{ id:2, title:'Design Tokens', desc:'Cores, espaçamentos e animações 100% dinâmicos.', color:'#7c3aed' },
|
||||||
|
{ id:3, title:'Scrollytelling', desc:'Câmera 3D guiada pelo scroll — experiência cinemática.',color:'#60a5fa' },
|
||||||
|
{ id:4, title:'Micro-interações',desc:'Botões vivos, luzes pulsantes, partículas orgânicas.',color:'#c084fc' },
|
||||||
|
{ id:5, title:'WCAG / A11y', desc:'Acessibilidade por padrão — semântica, foco, ARIA.',color:'#34d399' },
|
||||||
|
{ id:6, title:'Core Web Vitals',desc:'Hot path otimizado — 90+ Lighthouse por padrão.', color:'#fbbf24' },
|
||||||
|
]
|
||||||
|
|
||||||
|
export function FeaturesScene3d() {
|
||||||
|
const groupRef = useRef<THREE.Group>(null!)
|
||||||
|
|
||||||
|
useFrame((_s, dt) => {
|
||||||
|
if (groupRef.current) groupRef.current.rotation.y += dt * 0.015
|
||||||
|
})
|
||||||
|
|
||||||
|
// posições em círculo para efeito globo
|
||||||
|
const positions = FEATURES.map((_, i) => {
|
||||||
|
const angle = (i / FEATURES.length) * Math.PI * 2 - Math.PI / 2
|
||||||
|
const r = 3.2
|
||||||
|
return [Math.cos(angle) * r, Math.sin(angle) * r * 0.45, Math.sin(angle) * 0.8] as [number, number, number]
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group ref={groupRef}>
|
||||||
|
{FEATURES.map((f, i) => (
|
||||||
|
<Float key={f.id} speed={0.8 + i * 0.1} rotationIntensity={0.05} floatIntensity={0.35}>
|
||||||
|
<group position={positions[i]}>
|
||||||
|
<mesh castShadow>
|
||||||
|
<boxGeometry args={[0.75, 0.75, 0.75]} />
|
||||||
|
<meshStandardMaterial
|
||||||
|
color={f.color}
|
||||||
|
metalness={0.85}
|
||||||
|
roughness={0.1}
|
||||||
|
emissive={f.color}
|
||||||
|
emissiveIntensity={0.22}
|
||||||
|
/>
|
||||||
|
</mesh>
|
||||||
|
<pointLight color={f.color} intensity={1.2} distance={5} decay={2} />
|
||||||
|
<Text
|
||||||
|
position={[0, 0.85, 0]}
|
||||||
|
fontSize={0.3}
|
||||||
|
color="#e4e4e7"
|
||||||
|
anchorX="center"
|
||||||
|
maxWidth={2.5}
|
||||||
|
>
|
||||||
|
{f.title}
|
||||||
|
</Text>
|
||||||
|
</group>
|
||||||
|
</Float>
|
||||||
|
))}
|
||||||
|
</group>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
/** Organismo: HeroScene3d
|
||||||
|
* Cena hero com partículas + floating mesh + luzes.
|
||||||
|
* Controlado pelo scroll — a câmera viaja conforme o usuário rola.
|
||||||
|
*/
|
||||||
|
import { useRef } from 'react'
|
||||||
|
import { useFrame, useThree } from '@react-three/fiber'
|
||||||
|
import { useScroll } from '@react-three/drei'
|
||||||
|
import { ParticleField } from '../molecules/ParticleField'
|
||||||
|
import { FloatingMesh } from '../molecules/FloatingMesh'
|
||||||
|
import { LightGlow } from '../atoms/LightGlow'
|
||||||
|
|
||||||
|
export function HeroScene3d() {
|
||||||
|
const { camera } = useThree()
|
||||||
|
const scroll = useScroll()
|
||||||
|
const t = useRef(0)
|
||||||
|
|
||||||
|
useFrame((_s, dt) => {
|
||||||
|
t.current += dt
|
||||||
|
// Scroll-driven camera
|
||||||
|
const offset = scroll.offset // 0 → 1 conforme scroll na página
|
||||||
|
camera.position.z = 12 - offset * 4 // 12 → 8
|
||||||
|
camera.position.y = offset * -3 // 0 → -3
|
||||||
|
camera.lookAt(0, offset * -2, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Luzes */}
|
||||||
|
<ambientLight intensity={0.4} color="#2563eb" />
|
||||||
|
<directionalLight position={[8, 10, 5]} intensity={1.0} color="#ffffff" castShadow />
|
||||||
|
<LightGlow color="#2563eb" intensity={2.5} position={[-4, 3, 2]} distance={18} />
|
||||||
|
<LightGlow color="#7c3aed" intensity={2.0} position={[4, -2, 3]} distance={18} />
|
||||||
|
|
||||||
|
{/* Partículas de fundo — estrelas ao fundo */}
|
||||||
|
<ParticleField count={3000} spread={40} color="#93c5fd" size={0.035} speed={0.1} />
|
||||||
|
|
||||||
|
{/* Objeto hero — torus flutuante central */}
|
||||||
|
<FloatingMesh
|
||||||
|
geometry="torus"
|
||||||
|
size={3.5}
|
||||||
|
position={[0, 0.5, -2]}
|
||||||
|
color="#2563eb"
|
||||||
|
emissive="#1e3a8a"
|
||||||
|
floatSpeed={0.5}
|
||||||
|
floatAmp={0.25}
|
||||||
|
spinSpeed={0.08}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Anéis orbitais */}
|
||||||
|
<OrbitalRing radius={5} color="#7c3aed" speed={0.05} tiltX={Math.PI / 3} />
|
||||||
|
<OrbitalRing radius={6.5} color="#2563eb" speed={-0.03} tiltX={-Math.PI / 5} />
|
||||||
|
|
||||||
|
{/* Cubo menor complementar */}
|
||||||
|
<FloatingMesh
|
||||||
|
geometry="box"
|
||||||
|
size={1.2}
|
||||||
|
position={[-3.5, -1, 0]}
|
||||||
|
color="#7c3aed"
|
||||||
|
emissive="#4c1d95"
|
||||||
|
floatSpeed={0.8}
|
||||||
|
spinSpeed={0.2}
|
||||||
|
showGlow={false}
|
||||||
|
/>
|
||||||
|
<FloatingMesh
|
||||||
|
geometry="sphere"
|
||||||
|
size={0.9}
|
||||||
|
position={[3.2, 1.5, -1]}
|
||||||
|
color="#60a5fa"
|
||||||
|
emissive="#1e40af"
|
||||||
|
floatSpeed={1.0}
|
||||||
|
spinSpeed={0.3}
|
||||||
|
showGlow={false}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Componente auxiliar: anel orbital */
|
||||||
|
function OrbitalRing({
|
||||||
|
radius = 5,
|
||||||
|
color = '#7c3aed',
|
||||||
|
speed = 0.05,
|
||||||
|
tiltX = Math.PI / 4,
|
||||||
|
}: {
|
||||||
|
radius?: number
|
||||||
|
color? : string
|
||||||
|
speed? : number
|
||||||
|
tiltX? : number
|
||||||
|
}) {
|
||||||
|
const ringRef = useRef<any>(null!)
|
||||||
|
useFrame((_s, dt) => {
|
||||||
|
if (ringRef.current)
|
||||||
|
ringRef.current.rotation.z += dt * speed
|
||||||
|
})
|
||||||
|
return (
|
||||||
|
<mesh ref={ringRef} rotation={[tiltX, 0, 0]}>
|
||||||
|
<torusGeometry args={[radius, 0.04, 16, 200]} />
|
||||||
|
<meshStandardMaterial color={color} emissive={color} emissiveIntensity={0.15} transparent opacity={0.5} />
|
||||||
|
</mesh>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { HeroScene3d } from './HeroScene3d'
|
||||||
|
export { FeaturesScene3d } from './FeaturesScene3d'
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/** Template: ThreePage
|
||||||
|
* Junta Canvas 3D (fundo) + overlay React 2D (UI).
|
||||||
|
* O overlay fica acima do canvas com pointer-events controlados.
|
||||||
|
*/
|
||||||
|
import { ReactNode, CSSProperties } from 'react'
|
||||||
|
import { SceneCanvas } from './SceneCanvas'
|
||||||
|
|
||||||
|
function overlayStyle: CSSProperties = {
|
||||||
|
position : 'absolute',
|
||||||
|
top : 0,
|
||||||
|
left : 0,
|
||||||
|
width : '100%',
|
||||||
|
height : '100%',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
zIndex : 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PageProps {
|
||||||
|
canvasChildren : ReactNode // dentro do Canvas (3D)
|
||||||
|
overlayChildren: ReactNode // overlay 2D
|
||||||
|
orbitControls ?: boolean
|
||||||
|
scrollPages ?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ThreePage({
|
||||||
|
canvasChildren,
|
||||||
|
overlayChildren,
|
||||||
|
orbitControls = false,
|
||||||
|
scrollPages = 6,
|
||||||
|
}: PageProps) {
|
||||||
|
return (
|
||||||
|
<main style={{ position: 'relative', width: '100%', minHeight: '100vh', overflow: 'hidden' }}>
|
||||||
|
<SceneCanvas orbitControls={scrollPages} scrollPages={scrollPages}>
|
||||||
|
{canvasChildren}
|
||||||
|
</SceneCanvas>
|
||||||
|
|
||||||
|
{/* Overlay 2D — pointer-events:auto só nos elementos interativos */}
|
||||||
|
<div style={overlayStyle}>
|
||||||
|
{overlayChildren}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
/** Template: SceneCanvas
|
||||||
|
* Canvas React Three Fiber — orquestra cena, câmera e controles.
|
||||||
|
* Aceita children como organisms/atoms 3D.
|
||||||
|
*/
|
||||||
|
import { Canvas } from '@react-three/fiber'
|
||||||
|
import { OrbitControls, ScrollControls, Environment, Float } from '@react-three/drei'
|
||||||
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
|
interface SceneCanvasProps {
|
||||||
|
children: ReactNode
|
||||||
|
orbitControls?: boolean
|
||||||
|
scrollPages?: number
|
||||||
|
scrollDamping?: number
|
||||||
|
environment?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SceneCanvas({
|
||||||
|
children,
|
||||||
|
orbitControls = false,
|
||||||
|
scrollPages = 5,
|
||||||
|
scrollDamping = 0.4,
|
||||||
|
environment = null,
|
||||||
|
}: SceneCanvasProps) {
|
||||||
|
return (
|
||||||
|
<Canvas
|
||||||
|
shadows
|
||||||
|
camera={{ fov: 60, near: 0.1, far: 100 }}
|
||||||
|
gl={{ antialias: true, alpha: false }}
|
||||||
|
dpr={[1, 2]}
|
||||||
|
>
|
||||||
|
{/* Cinemática de cor */}
|
||||||
|
<color attach="background" args={['#050510']} />
|
||||||
|
|
||||||
|
{/* Névoa para profundidade */}
|
||||||
|
<fog attach="fog" args={['#050510', 8, 40]} />
|
||||||
|
|
||||||
|
{environment && <Environment preset={environment} background />}
|
||||||
|
{orbitControls && <OrbitControls enablePan={false} enableZoom={false} maxPolarAngle={Math.PI / 1.8} />}
|
||||||
|
|
||||||
|
{/* Scroll driver — children recebem offset do scroll */}
|
||||||
|
<ScrollControls pages={scrollPages} damping={scrollDamping}>
|
||||||
|
{children}
|
||||||
|
</ScrollControls>
|
||||||
|
</Canvas>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom/client'
|
||||||
|
import App from './App'
|
||||||
|
import './styles/globals.css'
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
)
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;900&family=JetBrains+Mono:wght@400;500&display=swap');
|
||||||
|
|
||||||
|
/* ── Reset + Globals ─────────────────────────────────────────────── */
|
||||||
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg: #050510;
|
||||||
|
--text: #e4e4e7;
|
||||||
|
--text-dim: #94a3b8;
|
||||||
|
--accent: #2563eb;
|
||||||
|
--accent-glow: rgba(37,99,235,.35);
|
||||||
|
--secondary: #7c3aed;
|
||||||
|
--grid: rgba(255,255,255,.03);
|
||||||
|
--radius-md: 8px;
|
||||||
|
--radius-lg: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body, #root {
|
||||||
|
width:100%; height:100%;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
font-family: 'Inter', system-ui, sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
overflow-x: hidden;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Grid de fundo sutil ─────────────────────────────────────────── */
|
||||||
|
body::before {
|
||||||
|
content:'';
|
||||||
|
position:fixed;
|
||||||
|
inset:0; z-index:0;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(var(--grid) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, var(--grid) 1px, transparent 1px);
|
||||||
|
background-size: 64px 64px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Seleção custom ──────────────────────────────────────────────── */
|
||||||
|
::selection { background:#2563eb44; color:#fff }
|
||||||
|
|
||||||
|
/* ── Focus visible (A11y) ────────────────────────────────────────── */
|
||||||
|
:focus-visible {
|
||||||
|
outline: 2px solid var(--accent);
|
||||||
|
outline-offset: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Scrollbar ───────────────────────────────────────────────────── */
|
||||||
|
::-webkit-scrollbar { width: 6px }
|
||||||
|
::-webkit-scrollbar-track { background: transparent }
|
||||||
|
::-webkit-scrollbar-thumb { background: #334155; border-radius: 3px }
|
||||||
|
::-webkit-scrollbar-thumb:hover { background: #475569 }
|
||||||
|
|
||||||
|
/* ── Animações custom ────────────────────────────────────────────── */
|
||||||
|
@keyframes float {
|
||||||
|
0%,100% { transform: translateY(0) }
|
||||||
|
50% { transform: translateY(-8px) }
|
||||||
|
}
|
||||||
|
@keyframes glow-pulse {
|
||||||
|
0%,100% { box-shadow: 0 0 20px rgba(37,99,235,.3) }
|
||||||
|
50% { box-shadow: 0 0 40px rgba(37,99,235,.6), 0 0 80px rgba(124,58,237,.2) }
|
||||||
|
}
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% { background-position: -200% center }
|
||||||
|
100%{ background-position: 200% center }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Modo Dia ────────────────────────────────────────────────────── */
|
||||||
|
[data-theme="light"] {
|
||||||
|
--bg : #f8fafc;
|
||||||
|
--text : #0f172a;
|
||||||
|
--text-dim: #64748b;
|
||||||
|
--accent : #2563eb;
|
||||||
|
--accent-glow: rgba(37,99,235,.12);
|
||||||
|
--grid: rgba(0,0,0,.04);
|
||||||
|
}
|
||||||
|
[data-theme="light"] body::before {
|
||||||
|
filter: invert(1); opacity:.25;
|
||||||
|
}
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
/** 🔮 Design Tokens — pulse-3d-landing
|
||||||
|
* Todos os valores do design system centralizados.
|
||||||
|
* Tokens 3D (material, luz, câmera) convivem com tokens 2D (cores, tipografia).
|
||||||
|
* Tema dinâmico via CSS variables + Theme Context React.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const tokens = {
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
// 1 · ESPAÇAMENTO (8px grid — múltiplos de 8)
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
space: {
|
||||||
|
0: '0px',
|
||||||
|
1: '4px',
|
||||||
|
2: '8px',
|
||||||
|
3: '16px',
|
||||||
|
4: '24px',
|
||||||
|
5: '32px',
|
||||||
|
6: '48px',
|
||||||
|
7: '64px',
|
||||||
|
8: '96px',
|
||||||
|
9: '128px',
|
||||||
|
10: '192px',
|
||||||
|
11: '256px',
|
||||||
|
},
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
// 2 · TIPOGRAFIA (escala modular 1.25)
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
font: {
|
||||||
|
family: {
|
||||||
|
display : "'Inter', 'Helvetica Neue', system-ui, sans-serif",
|
||||||
|
body : "'Inter', 'Helvetica Neue', system-ui, sans-serif",
|
||||||
|
mono : "'JetBrains Mono', 'Fira Code', monospace",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
xs : '0.75rem', // 12px
|
||||||
|
sm : '0.875rem', // 14px
|
||||||
|
base : '1rem', // 16px
|
||||||
|
lg : '1.25rem', // 20px
|
||||||
|
xl : '1.563rem', // 25px
|
||||||
|
2xl : '1.953rem', // 31px
|
||||||
|
3xl : '2.441rem', // 39px
|
||||||
|
4xl : '3.052rem', // 49px
|
||||||
|
5xl : '3.815rem', // 61px
|
||||||
|
6xl : '4.768rem', // 76px
|
||||||
|
},
|
||||||
|
weight: {
|
||||||
|
light : 300,
|
||||||
|
normal : 400,
|
||||||
|
medium : 500,
|
||||||
|
semibd : 600,
|
||||||
|
bold : 700,
|
||||||
|
black : 900,
|
||||||
|
},
|
||||||
|
line: {
|
||||||
|
tight : '1.15',
|
||||||
|
normal : '1.6',
|
||||||
|
loose : '1.8',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
// 3 · CORES (paleta HSL — fácil de theme-swap)
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
color: {
|
||||||
|
// base
|
||||||
|
black : 'hsl(0, 0%, 0%)',
|
||||||
|
white : 'hsl(0, 0%, 100%)',
|
||||||
|
|
||||||
|
// neutras
|
||||||
|
gray50 : 'hsl(210, 40%, 98%)',
|
||||||
|
gray100 : 'hsl(210, 20%, 94%)',
|
||||||
|
gray200 : 'hsl(210, 15%, 86%)',
|
||||||
|
gray300 : 'hsl(210, 12%, 74%)',
|
||||||
|
gray400 : 'hsl(210, 10%, 55%)',
|
||||||
|
gray500 : 'hsl(210, 9%, 44%)',
|
||||||
|
gray600 : 'hsl(210, 8%, 34%)',
|
||||||
|
gray700 : 'hsl(210, 10%, 27%)',
|
||||||
|
gray800 : 'hsl(210, 12%, 15%)',
|
||||||
|
gray900 : 'hsl(210, 14%, 8%)',
|
||||||
|
|
||||||
|
// accent — eléctrico azul (#2563eb → hsl 217 91% 60%)
|
||||||
|
accent : 'hsl(217, 91%, 60%)',
|
||||||
|
accentLight : 'hsl(217, 91%, 75%)',
|
||||||
|
accentDark : 'hsl(217, 91%, 45%)',
|
||||||
|
accentMuted : 'hsl(217, 80%, 94%)',
|
||||||
|
|
||||||
|
// secondary — violeta (#7c3aed → hsl 258 90% 66%)
|
||||||
|
secondary : 'hsl(258, 90%, 66%)',
|
||||||
|
secondaryMuted: 'hsl(258, 75%, 92%)',
|
||||||
|
|
||||||
|
// semânticos
|
||||||
|
success : 'hsl(142, 71%, 45%)',
|
||||||
|
warning : 'hsl(38, 92%, 50%)',
|
||||||
|
danger : 'hsl(0, 84%, 60%)',
|
||||||
|
info : 'hsl(199, 89%, 48%)',
|
||||||
|
},
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
// 4 · ELEVATION / SOMBRA
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
shadow: {
|
||||||
|
none : 'none',
|
||||||
|
sm : '0 1px 2px rgba(0,0,0,.12)',
|
||||||
|
md : '0 4px 12px rgba(0,0,0,.15)',
|
||||||
|
lg : '0 8px 24px rgba(0,0,0,.18)',
|
||||||
|
xl : '0 16px 48px rgba(0,0,0,.22)',
|
||||||
|
glow : '0 0 40px rgba(37,99,235,.30)', // accent glow
|
||||||
|
glow2 : '0 0 60px rgba(124,58,237,.25)', // secondary glow
|
||||||
|
},
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
// 5 · BORDER RADIUS
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
radius: {
|
||||||
|
none : '0px',
|
||||||
|
sm : '4px',
|
||||||
|
md : '8px',
|
||||||
|
lg : '16px',
|
||||||
|
xl : '24px',
|
||||||
|
2xl : '32px',
|
||||||
|
full : '9999px',
|
||||||
|
},
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
// 6 · 3D — MATERIAIS (material tokens → Three.js props)
|
||||||
|
// Todos os valores normalizados [0,1]
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
material3d: {
|
||||||
|
// Atmosfera geral da cena
|
||||||
|
scene: {
|
||||||
|
background : '#050510', // midnight-abyss
|
||||||
|
fogNear : 8,
|
||||||
|
fogFar : 40,
|
||||||
|
fogColor : '#050510',
|
||||||
|
},
|
||||||
|
// Luz ambiente
|
||||||
|
ambient: {
|
||||||
|
intensity : 0.4,
|
||||||
|
color : '#2563eb', // accent tênue
|
||||||
|
},
|
||||||
|
// Luz direcional principal
|
||||||
|
directional: {
|
||||||
|
intensity : 1.2,
|
||||||
|
color : '#ffffff',
|
||||||
|
pos : [5, 10, 7] as [number, number, number],
|
||||||
|
},
|
||||||
|
// Ponto de luz colorida 1
|
||||||
|
point1: {
|
||||||
|
intensity : 2.5,
|
||||||
|
color : '#2563eb', // accent
|
||||||
|
pos : [-4, 3, 2] as [number, number, number],
|
||||||
|
distance : 20,
|
||||||
|
decay : 2,
|
||||||
|
},
|
||||||
|
// Ponto de luz colorida 2
|
||||||
|
point2: {
|
||||||
|
intensity : 2.0,
|
||||||
|
color : '#7c3aed', // secondary
|
||||||
|
pos : [4, -2, 3] as [number, number, number],
|
||||||
|
distance : 20,
|
||||||
|
decay : 2,
|
||||||
|
},
|
||||||
|
// Padrão para geometrias flutuantes
|
||||||
|
floating: {
|
||||||
|
roughness : 0.08, // quase polido — reflexivo
|
||||||
|
metalness : 0.75, // metálico
|
||||||
|
emissive : '#1e3a8a',
|
||||||
|
emissiveIntensity: 0.3,
|
||||||
|
},
|
||||||
|
// Padrão para partículas / estrelas
|
||||||
|
particle: {
|
||||||
|
size : 0.04,
|
||||||
|
color : '#93c5fd',
|
||||||
|
opacity : 0.8,
|
||||||
|
},
|
||||||
|
// Padrão para texto flutuante 3D (CSS3DRenderer)
|
||||||
|
text3d: {
|
||||||
|
color : '#e4e4e7',
|
||||||
|
fontSize : '2rem',
|
||||||
|
fontWeight : 700,
|
||||||
|
letterSpacing: '0.05em',
|
||||||
|
textShadow : '0 0 30px rgba(37,99,235,.6)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
// 7 · 3D — CÂMERA
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
camera3d: {
|
||||||
|
fov : 60,
|
||||||
|
near : 0.1,
|
||||||
|
far : 100,
|
||||||
|
// Posição inicial
|
||||||
|
position : [0, 0, 12] as [number, number, number],
|
||||||
|
// Limites do scroll驱动
|
||||||
|
scrollRange : { start: [0, 0, 12], end: [0, -8, 6] },
|
||||||
|
lookAt : [0, 0, 0],
|
||||||
|
},
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
// 8 · ANIMAÇÃO (Framer Motion / GSAP)
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
animation: {
|
||||||
|
// durações
|
||||||
|
instant : 0.1,
|
||||||
|
quick : 0.25,
|
||||||
|
normal : 0.4,
|
||||||
|
slow : 0.7,
|
||||||
|
cinematic: 1.2,
|
||||||
|
// easing
|
||||||
|
easeOut : [0.16, 1, 0.3, 1], // ease-out-expo
|
||||||
|
easeInOut: [0.65, 0, 0.35, 1],
|
||||||
|
spring : { type: 'spring' as const, stiffness: 300, damping: 20 },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── CSS Scroll-Driven Variables (injetadas em :root) ─────────────
|
||||||
|
export const cssVariables: Record<string, string> = {
|
||||||
|
// Espelhamento de tokens em CSS para componentes 2D
|
||||||
|
'--space-1' : tokens.space[1],
|
||||||
|
'--space-2' : tokens.space[2],
|
||||||
|
'--space-3' : tokens.space[3],
|
||||||
|
'--space-4' : tokens.space[4],
|
||||||
|
'--space-5' : tokens.space[5],
|
||||||
|
'--color-accent' : tokens.color.accent,
|
||||||
|
'--color-accent-glow': tokens.shadow.glow,
|
||||||
|
'--radius-md' : tokens.radius.md,
|
||||||
|
'--radius-lg' : tokens.radius.lg,
|
||||||
|
'--font-display': tokens.font.family.display,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DesignTokens = typeof tokens
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: { host: true, port: 5173 }
|
||||||
|
})
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import Redis from 'ioredis'
|
||||||
|
const redis = new Redis({ host: 'redis', port: 6379, lazyConnect: true })
|
||||||
|
const QUEUE = 'dev-tasks'
|
||||||
|
const LOG = 'dev-logs'
|
||||||
|
|
||||||
|
function log(level, msg) {
|
||||||
|
const line = JSON.stringify({ ts: new Date().toISOString(), level, msg })
|
||||||
|
redis.publish(LOG, line).catch(() => {})
|
||||||
|
console.log(`[${level}] ${msg}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function claimTask(task) {
|
||||||
|
await redis.hset(`agent:agent-backend:task`, task.id, JSON.stringify(task))
|
||||||
|
await redis.hset(`agent:agent-backend`, 'status', 'busy')
|
||||||
|
await redis.hset(`agent:agent-backend`, 'current_task', task.title)
|
||||||
|
await redis.publish(LOG, JSON.stringify({ ts: new Date().toISOString(), level: 'AGENT', msg: `Backend pegou: "${task.title}"` }))
|
||||||
|
task.status = 'in_progress'
|
||||||
|
task.assignee = 'agent-backend'
|
||||||
|
await redis.set(`task:${task.id}`, JSON.stringify(task))
|
||||||
|
log('AGENT', `▶️ Processando: ${task.title}`)
|
||||||
|
await new Promise(r => setTimeout(r, Math.random() * 6000 + 3000))
|
||||||
|
task.status = 'done'
|
||||||
|
task.done_at = new Date().toISOString()
|
||||||
|
await redis.set(`task:${task.id}`, JSON.stringify(task))
|
||||||
|
await redis.hset(`agent:agent-backend`, 'status', 'idle')
|
||||||
|
await redis.hdel(`agent:agent-backend:task`, task.id)
|
||||||
|
await redis.hincrby('agent:agent-backend', 'tasks_done', 1)
|
||||||
|
await awaitingTask(task)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function awaitingTask(task) {
|
||||||
|
await redis.hset(`agent:agent-backend`, 'status', 'idle')
|
||||||
|
await redis.hdel(`agent:agent-backend:task`, task.id)
|
||||||
|
log('AGENT', `✔️ Concluído: ${task.title}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
await redis.connect()
|
||||||
|
await redis.set('agent:agent-backend', JSON.stringify({
|
||||||
|
id: 'agent-backend', role: 'backend', status: 'online',
|
||||||
|
started_at: new Date().toISOString(), tasks_done: 0,
|
||||||
|
}))
|
||||||
|
log('AGENT', 'Backend Agent online — aguardando tarefas')
|
||||||
|
while (true) {
|
||||||
|
const [_, taskId] = await redis.blpop(QUEUE, 60)
|
||||||
|
if (!taskId) { await new Promise(r => setTimeout(r, 1000)); continue }
|
||||||
|
const raw = await redis.get(`task:${taskId}`)
|
||||||
|
if (!raw) continue
|
||||||
|
const task = JSON.parse(raw)
|
||||||
|
if (task.domain !== 'backend' && task.domain !== 'fullstack') {
|
||||||
|
await redis.rpush(QUEUE, taskId); continue
|
||||||
|
}
|
||||||
|
await claimTask(task)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run().catch(e => { console.error(e); process.exit(1) })
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import Redis from 'ioredis'
|
||||||
|
import { execSync } from 'child_process'
|
||||||
|
const redis = new Redis({ host: 'redis', port: 6379, lazyConnect: true })
|
||||||
|
const QUEUE = 'dev-tasks'
|
||||||
|
const LOG = 'dev-logs'
|
||||||
|
|
||||||
|
function log(level, msg) {
|
||||||
|
const line = JSON.stringify({ ts: new Date().toISOString(), level, msg })
|
||||||
|
redis.publish(LOG, line).catch(() => {})
|
||||||
|
console.log(`[${level}] ${msg}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function exec(cmd) {
|
||||||
|
try { return execSync(cmd, { encoding: 'utf8', timeout: 30000 }).trim() }
|
||||||
|
catch(e) { return `ERROR: ${e.message}` }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processTask(task) {
|
||||||
|
task.status = 'in_progress'
|
||||||
|
task.assignee = 'agent-devops'
|
||||||
|
await redis.set(`task:${task.id}`, JSON.stringify(task))
|
||||||
|
log('AGENT', `▶️ Processando: ${task.title}`)
|
||||||
|
const result = exec(task.command || `echo "${task.title}"`)
|
||||||
|
task.result = result
|
||||||
|
task.status = 'done'
|
||||||
|
task.done_at = new Date().toISOString()
|
||||||
|
await redis.set(`task:${task.id}`, JSON.stringify(task))
|
||||||
|
log('AGENT', `✔️ Concluído: ${task.title}`)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
await redis.connect()
|
||||||
|
await redis.set('agent:agent-devops', JSON.stringify({
|
||||||
|
id: 'agent-devops', role: 'devops', status: 'online',
|
||||||
|
started_at: new Date().toISOString(), tasks_done: 0,
|
||||||
|
}))
|
||||||
|
log('AGENT', 'DevOps Agent online — aguardando tarefas')
|
||||||
|
while (true) {
|
||||||
|
const [_, tid] = await redis.blpop(QUEUE, 60)
|
||||||
|
if (!tid) { await new Promise(r => setTimeout(r, 1000)); continue }
|
||||||
|
const raw = await redis.get(`task:${tid}`)
|
||||||
|
if (!raw) continue
|
||||||
|
const task = JSON.parse(raw)
|
||||||
|
if (task.domain !== 'devops') {
|
||||||
|
await redis.rpush(QUEUE, tid); continue
|
||||||
|
}
|
||||||
|
await processTask(task)
|
||||||
|
await redis.hincrby('agent:agent-devops', 'tasks_done', 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run().catch(e => { console.error(e); process.exit(1) })
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import Redis from 'ioredis'
|
||||||
|
const redis = new Redis({ host: 'redis', port: 6379, lazyConnect: true })
|
||||||
|
const QUEUE = 'dev-tasks'
|
||||||
|
const LOG = 'dev-logs'
|
||||||
|
|
||||||
|
function log(level, msg) {
|
||||||
|
const line = JSON.stringify({ ts: new Date().toISOString(), level, msg })
|
||||||
|
redis.publish(LOG, line).catch(() => {})
|
||||||
|
console.log(`[${level}] ${msg}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function claimTask(task) {
|
||||||
|
await redis.hset(`agent:agent-frontend:task`, task.id, JSON.stringify(task))
|
||||||
|
await redis.hset(`agent:agent-frontend`, 'status', 'busy')
|
||||||
|
await redis.hset(`agent:agent-frontend`, 'current_task', task.title)
|
||||||
|
await redis.publish(LOG, JSON.stringify({ ts: new Date().toISOString(), level: 'AGENT', msg: `Frontend pegou: "${task.title}"` }))
|
||||||
|
task.status = 'in_progress'
|
||||||
|
task.assignee = 'agent-frontend'
|
||||||
|
await redis.set(`task:${task.id}`, JSON.stringify(task))
|
||||||
|
log('AGENT', `▶️ Processando: ${task.title}`)
|
||||||
|
await new Promise(r => setTimeout(r, Math.random() * 5000 + 2000))
|
||||||
|
task.status = 'done'
|
||||||
|
task.done_at = new Date().toISOString()
|
||||||
|
await redis.set(`task:${task.id}`, JSON.stringify(task))
|
||||||
|
await redis.hset(`agent:agent-frontend`, 'status', 'idle')
|
||||||
|
await redis.hdel(`agent:agent-frontend:task`, task.id)
|
||||||
|
awaitingTask(task)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function awaitingTask(task) {
|
||||||
|
await redis.hset(`agent:agent-frontend`, 'status', 'idle')
|
||||||
|
await redis.hdel(`agent:agent-frontend:task`, task.id)
|
||||||
|
log('AGENT', `✔️ Concluído: ${task.title}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
await redis.connect()
|
||||||
|
await redis.set('agent:agent-frontend', JSON.stringify({
|
||||||
|
id: 'agent-frontend', role: 'frontend', status: 'online',
|
||||||
|
started_at: new Date().toISOString(), tasks_done: 0,
|
||||||
|
}))
|
||||||
|
log('AGENT', 'Frontend Agent online — aguardando tarefas')
|
||||||
|
while (true) {
|
||||||
|
const [_, taskId] = await redis.blpop(QUEUE, 60)
|
||||||
|
if (!taskId) { await new Promise(r => setTimeout(r, 1000)); continue }
|
||||||
|
const raw = await redis.get(`task:${taskId}`)
|
||||||
|
if (!raw) continue
|
||||||
|
const task = JSON.parse(raw)
|
||||||
|
if (task.domain !== 'frontend' && task.domain !== 'fullstack') {
|
||||||
|
await redis.rpush(QUEUE, taskId); continue
|
||||||
|
}
|
||||||
|
await claimTask(task)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run().catch(e => { console.error(e); process.exit(1) })
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "pulse-agents",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.1",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"agent-frontend": "node /app/agents/frontend/agent.js",
|
||||||
|
"agent-backend": "node /app/agents/backend/agent.js",
|
||||||
|
"agent-devops": "node /app/agents/devops/agent.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"ioredis": "^5.4.1",
|
||||||
|
"uuid": "^10.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "pulse-dev-backend",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.1",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "tsx watch --dir ./src server.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^5.1.0",
|
||||||
|
"ioredis": "^5.4.1",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"helmet": "^8.0.0",
|
||||||
|
"uuid": "^10.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"tsx": "^4.19.2",
|
||||||
|
"@types/express": "^5.0.0",
|
||||||
|
"@types/cors": "^2.8.17",
|
||||||
|
"@types/uuid": "^10.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import express from 'express'
|
||||||
|
import cors from 'cors'
|
||||||
|
import helmet from 'helmet'
|
||||||
|
import Redis from 'ioredis'
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
|
||||||
|
const app = express()
|
||||||
|
app.use(cors())
|
||||||
|
app.use(helmet())
|
||||||
|
app.use(express.json())
|
||||||
|
|
||||||
|
// ─── Redis ──────────────────────────────────────────────────────────
|
||||||
|
const redis = new Redis({ host: 'redis', port: 6379, lazyConnect: true })
|
||||||
|
redis.on('error', (err) => console.error('[Redis] error:', err.message))
|
||||||
|
redis.on('ready', () => console.log('[Redis] conectado'))
|
||||||
|
|
||||||
|
// ─── Task Queue ─────────────────────────────────────────────────────
|
||||||
|
const TASK_QUEUE = 'dev-tasks'
|
||||||
|
const LOG_CHANNEL = 'dev-logs'
|
||||||
|
|
||||||
|
function log(level, msg) {
|
||||||
|
const line = JSON.stringify({ ts: new Date().toISOString(), level, msg })
|
||||||
|
redis.publish(LOG_CHANNEL, line).catch(() => {})
|
||||||
|
console.log(`[${level}] ${msg}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /tasks — lista todas
|
||||||
|
// POST /tasks — cria tarefa
|
||||||
|
// PUT /tasks/:id — atualiza status
|
||||||
|
// GET /agents — lista agentes conectados
|
||||||
|
// GET /logs/:limit — logs recentes
|
||||||
|
|
||||||
|
app.get('/tasks', async (_req, res) => {
|
||||||
|
const keys = await redis.keys('task:*')
|
||||||
|
const tasks = []
|
||||||
|
for (const k of keys) {
|
||||||
|
const t = await redis.get(k)
|
||||||
|
if (t) tasks.push(JSON.parse(t))
|
||||||
|
}
|
||||||
|
tasks.sort((a, b) => b.updated_at?.localeCompare(a.updated_at) || 0)
|
||||||
|
res.json(tasks)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.post('/tasks', async (req, res) => {
|
||||||
|
const id = uuidv4()
|
||||||
|
const task = {
|
||||||
|
id, title: req.body.title, type: req.body.type || 'feature',
|
||||||
|
priority: req.body.priority || 'medium', status: 'todo',
|
||||||
|
domain: req.body.domain || 'fullstack', assignee: null,
|
||||||
|
created_at: new Date().toISOString(), updated_at: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
await redis.set(`task:${id}`, JSON.stringify(task))
|
||||||
|
await redis.rpush('task-queue', id)
|
||||||
|
log('TASK', `Nova tarefa: "${task.title}" [${task.type}]`)
|
||||||
|
res.status(201).json(task)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.put('/tasks/:id', async (req, res) => {
|
||||||
|
const key = `task:${req.params.id}`
|
||||||
|
const raw = await redis.get(key)
|
||||||
|
if (!raw) return res.status(404).json({ error: 'Não encontrada' })
|
||||||
|
const task = JSON.parse(raw)
|
||||||
|
Object.assign(task, req.body, { updated_at: new Date().toISOString() })
|
||||||
|
await redis.set(key, JSON.stringify(task))
|
||||||
|
log('INFO', `Tarefa atualizada: "${task.title}" status=${task.status}`)
|
||||||
|
res.json(task)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/agents', async (_req, res) => {
|
||||||
|
const keys = await redis.keys('agent:*')
|
||||||
|
const agents = []
|
||||||
|
for (const k of keys) {
|
||||||
|
const a = await redis.get(k)
|
||||||
|
if (a) agents.push(JSON.parse(a))
|
||||||
|
}
|
||||||
|
res.json(agents)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/health', (_req, res) => {
|
||||||
|
res.json({ status: 'ok', ts: new Date().toISOString() })
|
||||||
|
})
|
||||||
|
|
||||||
|
// Promover tarefa automaticamente ao criar/modificar
|
||||||
|
app.use('*', (req, res) => { res.status(404).json({ error: 'Not found' }) })
|
||||||
|
|
||||||
|
const PORT = 3001
|
||||||
|
app.listen(PORT, '0.0.0.0', () => {
|
||||||
|
// Registrar agente backend
|
||||||
|
;(async () => {
|
||||||
|
try { await redis.connect() } catch (e) { console.error('Redis connect:', e.message) }
|
||||||
|
await redis.set('agent:dev-backend', JSON.stringify({
|
||||||
|
id: 'dev-backend', role: 'backend', status: 'online',
|
||||||
|
started_at: new Date().toISOString(),
|
||||||
|
}))
|
||||||
|
log('AGENT', `Backend API online :${PORT}`)
|
||||||
|
log('AGENT', `Redis conectado — queue: ${TASK_QUEUE}`)
|
||||||
|
})()
|
||||||
|
})
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="pt-BR">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>Pulse Dev — Frontend</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body { font-family: 'Segoe UI', system-ui, sans-serif; background: #0f1117; color: #e4e4e7; }
|
||||||
|
nav { background: #1e293b; padding: 12px 24px; display: flex; gap: 20px; border-bottom: 1px solid #334155; }
|
||||||
|
nav a { color: #94a3b8; text-decoration: none; font-size: 14px; }
|
||||||
|
nav a.active { color: #60a5fa; font-weight: 600; }
|
||||||
|
section { padding: 32px 24px; max-width: 960px; margin: 0 auto; }
|
||||||
|
h1 { font-size: 28px; margin-bottom: 8px; }
|
||||||
|
h2 { font-size: 18px; margin: 24px 0 12px; color: #60a5fa; }
|
||||||
|
p { color: #94a3b8; line-height: 1.7; font-size: 14px; }
|
||||||
|
.status-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 12px; margin-top: 16px; }
|
||||||
|
.card { background: #1e293b; border: 1px solid #334155; border-radius: 8px; padding: 16px; }
|
||||||
|
.card b { color: #60a5fa; font-size: 20px; }
|
||||||
|
.card small { color: #64748b; font-size: 12px; }
|
||||||
|
.badge { display: inline-block; padding: 2px 8px; border-radius: 99px; font-size: 11px; margin: 2px; }
|
||||||
|
.ok { background: #14532d; color: #86efac; }
|
||||||
|
.warn{ background: #78350f; color: #fde68a; }
|
||||||
|
.err { background: #7f1d1d; color: #fca5a5; }
|
||||||
|
pre { background: #0f1117; border: 1px solid #334155; border-radius: 6px; padding: 16px; font-size: 13px; overflow-x: auto; color: #a5f3fc; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav>
|
||||||
|
<a href="#overview">Visão Geral</a>
|
||||||
|
<a href="#agents">Agentes</a>
|
||||||
|
<a href="#stacks">Stacks</a>
|
||||||
|
<a href="#flows">Fluxos</a>
|
||||||
|
</nav>
|
||||||
|
<section id="overview">
|
||||||
|
<h1>⚡ Pulse Frontend</h1>
|
||||||
|
<p>Ambiente de desenvolvimento full-stack com hot reload, agents paralelos e taskboard em tempo real.</p>
|
||||||
|
<div class="status-grid">
|
||||||
|
<div class="card"><small>Redis</small><br><b id="redis-badge"><span class="warn badge">conectando…</span></b></div>
|
||||||
|
<div class="card"><small>TaskBoard</small><br><b>board.octal.tec.br</b></div>
|
||||||
|
<div class="card"><small>Backend API</small><br><b id="api-status">verificando…</b></div>
|
||||||
|
<div class="card"><small>Hot Reload</small><br><b id="hmr-status">aguardando</b></div>
|
||||||
|
<div class="card"><small>Backend Agent</small><br><b id="be-status">—</b></div>
|
||||||
|
<div class="card"><small>Frontend Agent</small><br><b id="fe-status">—</b></div>
|
||||||
|
<div class="card"><small>DevOps Agent</small><br><b id="ops-status">—</b></div>
|
||||||
|
<div class="card"><small>Tasks na Queue</small><br><b id="queue-count">—</b></div>
|
||||||
|
</div>
|
||||||
|
<h2>Stack Swarm / Clusters</h2>
|
||||||
|
<pre id="stack-info">Carregando…</pre>
|
||||||
|
<h2>Últimos Logs</h2>
|
||||||
|
<pre id="logs">Aguardando eventos…</pre>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const API = 'http://redis:6379'
|
||||||
|
const BACKEND_URL = 'http://localhost:3001'
|
||||||
|
|
||||||
|
async function update() {
|
||||||
|
try {
|
||||||
|
const [agentsRes, stacksRes, logsRes] = await Promise.allSettled([
|
||||||
|
fetch(`${BACKEND_URL}/agents`),
|
||||||
|
fetch(`${BACKEND_URL}/tasks?redis=1`),
|
||||||
|
fetch(`${BACKEND_URL}/health`),
|
||||||
|
])
|
||||||
|
if (agentsRes.status === 'fulfilled') {
|
||||||
|
const agents = await agentsRes.value.json()
|
||||||
|
const map = {};
|
||||||
|
agents.forEach(a => { map[a.id] = a; })
|
||||||
|
const setStatus = (id, badgeId, labelId) => {
|
||||||
|
const el = document.getElementById(badgeId)
|
||||||
|
if (!el) return
|
||||||
|
const a = map[id]
|
||||||
|
if (!a) return
|
||||||
|
const s = a.status
|
||||||
|
document.getElementById(labelId).textContent = a.role || s
|
||||||
|
el.innerHTML = `<span class="${s==='online'?'ok':s==='busy'?'warn':'err'} badge">${s}</span>`
|
||||||
|
}
|
||||||
|
setStatus('dev-backend', 'be-status', 'api-status')
|
||||||
|
setStatus('agent-frontend','fe-status','hmr-status')
|
||||||
|
setStatus('agent-devops', 'ops-status','ops-status')
|
||||||
|
}
|
||||||
|
if (stacksRes.status === 'fulfilled') {
|
||||||
|
const tasks = await stacksRes.value.json()
|
||||||
|
const counts = { todo: 0, doing: 0, done: 0 }
|
||||||
|
tasks.forEach(t => { if (counts[t.status] !== undefined) counts[t.status]++ })
|
||||||
|
document.getElementById('queue-count').textContent = counts.todo + counts.doing
|
||||||
|
}
|
||||||
|
} catch(e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getStackInfo() {
|
||||||
|
const r = await fetch(`${BACKEND_URL}/health`)
|
||||||
|
if (!r.ok) return 'Backend: timeout — verificar stack dev no Swarm'
|
||||||
|
return`Stack Swarm — Ambiente Dev Full-Stack
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ ORQUESTRADOR │ TaskBoard │ Redis │ Pipeline │
|
||||||
|
│ dock.pulse-ops│ board.oc. │ 6379 │ CI/CD → Gitea │
|
||||||
|
│ (auto) │ (nginx) │ (falback)│ GitHub Actions │
|
||||||
|
├─────────────────────────────────────────────────────────────────┤
|
||||||
|
│ FRONTEND │ BACKEND │ WORKERS │ Deploy │
|
||||||
|
│ Vite 5173 │ tsx:3001 │ 2 FE/2 BE│ docker stack deploy -c │
|
||||||
|
│ (hot reload) │ (hot reload│ 1 DevOps │ runbooks/dev-stack.yml │
|
||||||
|
│ │ + HMR) │ │ dev │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘`
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getLogs() {
|
||||||
|
try {
|
||||||
|
const r = await fetch(`${BACKEND_URL}/logs?limit=20`)
|
||||||
|
const lines = await r.json()
|
||||||
|
return lines.map(l => {
|
||||||
|
const ts = ts(l.ts || Date.now())
|
||||||
|
const col = { INFO:'#60a5fa', AGENT:'#22c55e', TASK:'#a78bfa', WARN:'#f0b429', ERROR:'#ef4444' }[l.level] || '#94a3b8'
|
||||||
|
return `\x1b[${col}m[${ts}]\x1b[0m [\x1b[${col}m${l.level}\x1b[0m] ${l.msg || JSON.stringify(l)}`
|
||||||
|
}).join('\n') || 'Sem logs ainda...'
|
||||||
|
} catch(e) { return 'Logs indisponíveis' }
|
||||||
|
}
|
||||||
|
|
||||||
|
function ts(ms) {
|
||||||
|
const d = new Date(ms)
|
||||||
|
return `${d.getHours().toString().padStart(2,'0')}:${d.getMinutes().toString().padStart(2,'0')}:${d.getSeconds().toString().padStart(2,'0')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
|
document.getElementById('stack-info').textContent = await getStackInfo()
|
||||||
|
await update()
|
||||||
|
setInterval(update, 3000)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "pulse-taskboard",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.1",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite --host 0.0.0.0 --port 5174",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"ioredis": "^5.4.1",
|
||||||
|
"uuid": "^10.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.3.12",
|
||||||
|
"@types/react-dom": "^18.3.1",
|
||||||
|
"@types/uuid": "^10.0.0",
|
||||||
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
"typescript": "~5.6.2",
|
||||||
|
"vite": "^6.0.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="pt-BR">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
|
<title>Pulse TaskBoard — Dev Orchestrator</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body { font-family: 'Segoe UI', system-ui, sans-serif; background: #0f1117; color: #e4e4e7; min-height: 100vh; }
|
||||||
|
header { background: #1e293b; padding: 12px 24px; border-bottom: 1px solid #334155; display: flex; justify-content: space-between; align-items: center; }
|
||||||
|
header h1 { font-size: 16px; color: #60a5fa; display: flex; align-items: center; gap: 8px; }
|
||||||
|
header h1 span { color: #94a3b8; font-weight: 400; font-size: 13px; }
|
||||||
|
.stats { display: flex; gap: 16px; font-size: 13px; }
|
||||||
|
.stat { background: #1e293b; border: 1px solid #334155; border-radius: 6px; padding: 4px 12px; }
|
||||||
|
.stat b { color: #60a5fa; }
|
||||||
|
main { padding: 20px; display: flex; gap: 16px; height: calc(100vh - 56px); }
|
||||||
|
.col { flex: 1; display: flex; flex-direction: column; border-radius: 8px; background: #1e293b; border: 1px solid #334155; overflow: hidden; }
|
||||||
|
.col-header { padding: 12px 16px; font-size: 13px; font-weight: 600; text-transform: uppercase; letter-spacing: .5px; border-bottom: 1px solid #334155; display: flex; justify-content: space-between; align-items: center; }
|
||||||
|
.col-header .count { background: #334155; padding: 2px 8px; border-radius: 99px; font-size: 11px; }
|
||||||
|
.todo .col-header { color: #f0b429; } .todo .count { background: #78350f; color: #fde68a; }
|
||||||
|
.doing .col-header { color: #3b82f6; } .doing .count { background: #1e3a5f; color: #93c5fd; }
|
||||||
|
.done .col-header { color: #22c55e; } .done .count { background: #14532d; color: #86efac; }
|
||||||
|
.tasks { flex: 1; overflow-y: auto; padding: 8px; display: flex; flex-direction: column; gap: 8px; }
|
||||||
|
.task { background: #0f1117; border: 1px solid #334155; border-radius: 8px; padding: 12px; cursor: pointer; transition: border-color .15s; }
|
||||||
|
.task:hover { border-color: #60a5fa; }
|
||||||
|
.task-title { font-size: 14px; font-weight: 500; margin-bottom: 4px; }
|
||||||
|
.task-meta { display: flex; gap: 8px; align-items: center; font-size: 11px; color: #94a3b8; }
|
||||||
|
.badge { padding: 1px 6px; border-radius: 4px; font-size: 10px; font-weight: 600; text-transform: uppercase; }
|
||||||
|
.badge-feature { background: #1e3a5f; color: #60a5fa; }
|
||||||
|
.badge-bug { background: #7f1d1d; color: #fca5a5; }
|
||||||
|
.badge-refactor{ background: #4a1d7f; color: #d8b4fe; }
|
||||||
|
.badge-test { background: #1a4f2e; color: #86efac; }
|
||||||
|
.badge-devops { background: #3f3713; color: #fde68a; }
|
||||||
|
.priority-critical { color: #ef4444; }
|
||||||
|
.priority-high { color: #f97316; }
|
||||||
|
.priority-medium { color: #fbbf24; }
|
||||||
|
.priority-low { color: #22c55e; }
|
||||||
|
.add-form { border-top: 1px solid #334155; padding: 12px; display: flex; gap: 8px; flex-wrap: wrap; }
|
||||||
|
.add-form input, .add-form select { background: #0f1117; border: 1px solid #334155; border-radius: 6px; padding: 8px 12px; color: #e4e4e7; font-size: 13px; outline: none; }
|
||||||
|
.add-form input:focus, .add-form select:focus { border-color: #60a5fa; }
|
||||||
|
.add-form input { flex: 1; min-width: 120px; }
|
||||||
|
.add-form button { background: #2563eb; border: none; border-radius: 6px; padding: 8px 16px; color: white; font-size: 13px; cursor: pointer; font-weight: 600; }
|
||||||
|
.add-form button:hover { background: #3b82f6; }
|
||||||
|
.logs-panel { width: 320px; background: #1e293b; border: 1px solid #334155; border-radius: 8px; overflow: hidden; display: flex; flex-direction: column; }
|
||||||
|
.logs-header { padding: 10px 14px; font-size: 12px; font-weight: 600; background: #0f1117; border-bottom: 1px solid #334155; color: #94a3b8; display: flex; justify-content: space-between; }
|
||||||
|
.logs { flex: 1; overflow-y: auto; padding: 8px; font-family: 'JetBrains Mono', 'Fira Code', monospace; font-size: 11px; line-height: 1.7; }
|
||||||
|
.log-line { display: flex; gap: 8px; }
|
||||||
|
.log-time { color: #475569; flex-shrink: 0; }
|
||||||
|
.log-level-INFO { color: #60a5fa; }
|
||||||
|
.log-level-WARN { color: #f0b429; }
|
||||||
|
.log-level-ERROR { color: #ef4444; }
|
||||||
|
.log-level-TASK { color: #a78bfa; }
|
||||||
|
.log-level-AGENT { color: #22c55e; }
|
||||||
|
::-webkit-scrollbar { width: 4px; } ::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-thumb { background: #334155; border-radius: 4px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<h1>⚡ Pulse TaskBoard <span>dev-orchestrator</span></h1>
|
||||||
|
<div class="stats">
|
||||||
|
<div class="stat">Tasks: <b id="stat-total">0</b></div>
|
||||||
|
<div class="stat">Ativas: <b id="stat-active">0</b></div>
|
||||||
|
<div class="stat">Agentes: <b id="stat-agents">3</b></div>
|
||||||
|
<div class="stat" id="redis-status">Redis: <b style="color:#ef4444">offline</b></div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<div class="col todo">
|
||||||
|
<div class="col-header">📋 A Fazer <span class="count" id="todo-count">0</span></div>
|
||||||
|
<div class="tasks" id="todo-tasks"></div>
|
||||||
|
<div class="add-form" id="todo-form">
|
||||||
|
<input id="todo-title" placeholder="Nova tarefa..." />
|
||||||
|
<select id="todo-priority"><option value="low">Baixa</option><option value="medium" selected>Média</option><option value="high">Alta</option><option value="critical">Crítica</option></select>
|
||||||
|
<select id="todo-type"><option value="feature">Feature</option><option value="bug">Bug</option><option value="refactor">Refactor</option><option value="test">Test</option><option value="devops">DevOps</option></select>
|
||||||
|
<button onclick="addTask()">+ Tarefa</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col doing">
|
||||||
|
<div class="col-header">🔨 Em Progresso <span class="count" id="doing-count">0</span></div>
|
||||||
|
<div class="tasks" id="doing-tasks"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col done">
|
||||||
|
<div class="col-header">✅ Concluídas <span class="count" id="done-count">0</span></div>
|
||||||
|
<div class="tasks" id="done-tasks"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="logs-panel">
|
||||||
|
<div class="logs-header">📡 Logs em Tempo Real <span id="log-count">0</span></div>
|
||||||
|
<div class="logs" id="logs"></div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
// ─── Redis (opcional: se estiver disponível) ──────────────────────────
|
||||||
|
const REDIS_URL = (location.hostname === 'localhost' || location.hostname === '127.0.0.1')
|
||||||
|
? 'ws://localhost:6379' : null;
|
||||||
|
let ws = null, useRedis = false;
|
||||||
|
|
||||||
|
function connectRedis() {
|
||||||
|
if (!REDIS_URL) return;
|
||||||
|
try {
|
||||||
|
ws = new WebSocket(REDIS_URL.replace('ws://', 'ws://').replace('http://', 'ws://'));
|
||||||
|
} catch(e) { useRedis = false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Task Store (localStorage + polling增量) ──────────────────────────
|
||||||
|
const LS_KEY = 'pulse-tasks';
|
||||||
|
function load() { try { return JSON.parse(localStorage.getItem(LS_KEY) || '[]'); } catch { return []; } }
|
||||||
|
function save(tasks) { localStorage.setItem(LS_KEY, JSON.stringify(tasks)); emitChange(); }
|
||||||
|
|
||||||
|
function emitChange() {
|
||||||
|
const tasks = load();
|
||||||
|
const counts = { todo: 0, doing: 0, done: 0 };
|
||||||
|
tasks.forEach(t => counts[t.status]++);
|
||||||
|
document.getElementById('todo-count').textContent = counts.todo;
|
||||||
|
document.getElementById('doing-count').textContent = counts.doing;
|
||||||
|
document.getElementById('done-count').textContent = counts.done;
|
||||||
|
document.getElementById('stat-total').textContent = tasks.length;
|
||||||
|
document.getElementById('stat-active').textContent = counts.todo + counts.doing;
|
||||||
|
renderColumns(tasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
function genId() { return crypto.randomUUID(); }
|
||||||
|
|
||||||
|
window.addTask = function() {
|
||||||
|
const title = document.getElementById('todo-title').value.trim();
|
||||||
|
if (!title) return;
|
||||||
|
const tasks = load();
|
||||||
|
task = {
|
||||||
|
id: genId(),
|
||||||
|
title,
|
||||||
|
type: document.getElementById('todo-type').value,
|
||||||
|
priority: document.getElementById('todo-priority').value,
|
||||||
|
status: 'todo',
|
||||||
|
assignee: null,
|
||||||
|
domain: document.getElementById('todo-type').value === 'devops' ? 'devops' : 'fullstack',
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
tasks.unshift(task);
|
||||||
|
save(tasks);
|
||||||
|
document.getElementById('todo-title').value = '';
|
||||||
|
addLog('TASK', `Tarefa criada: "${title}" [${task.type}]`);
|
||||||
|
};
|
||||||
|
|
||||||
|
function moveTask(id, newStatus) {
|
||||||
|
const tasks = load();
|
||||||
|
const t = tasks.find(x => x.id === id);
|
||||||
|
if (!t) return;
|
||||||
|
t.status = newStatus;
|
||||||
|
t.updated_at = new Date().toISOString();
|
||||||
|
save(tasks);
|
||||||
|
addLog('INFO', `${t.title} → ${newStatus}`);
|
||||||
|
// Publicar no Redis se conectado
|
||||||
|
if (ws && ws.readyState === 1) {
|
||||||
|
ws.send(JSON.stringify({ type: 'task_update', task: t }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderColumns(tasks) {
|
||||||
|
['todo', 'doing', 'done'].forEach(status => {
|
||||||
|
const col = document.getElementById(`${status}-tasks`);
|
||||||
|
const list = tasks.filter(t => t.status === status);
|
||||||
|
col.innerHTML = list.map(t => `
|
||||||
|
<div class="task" onclick="moveTask('${t.id}','${
|
||||||
|
t.status === 'todo' ? 'doing' : t.status === 'doing' ? 'done' : 'todo'
|
||||||
|
}')">
|
||||||
|
<div class="task-title">${t.title}</div>
|
||||||
|
<div class="task-meta">
|
||||||
|
<span class="badge badge-${t.type}">${t.type}</span>
|
||||||
|
<span class="priority-${t.priority}">${t.priority}</span>
|
||||||
|
<span>${new Date(t.created_at).toLocaleTimeString('pt-BR')}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Log Stream ─────────────────────────────────────────────────────
|
||||||
|
let logCount = 0;
|
||||||
|
const LOG_COLORS = { INFO:'#60a5fa', WARN:'#f0b429', ERROR:'#ef4444', TASK:'#a78bfa', AGENT:'#22c55e' };
|
||||||
|
|
||||||
|
function addLog(level, msg) {
|
||||||
|
const logs = document.getElementById('logs');
|
||||||
|
const ts = new Date().toLocaleTimeString('pt-BR', { hour12: false });
|
||||||
|
const line = document.createElement('div');
|
||||||
|
line.className = 'log-line';
|
||||||
|
line.innerHTML = `<span class="log-time">${ts}</span><span class="log-level-${level}" style="color:${LOG_COLORS[level]||'#aaa'}">[${level}]</span> <span>${msg}</span>`;
|
||||||
|
logs.appendChild(line);
|
||||||
|
logs.scrollTop = logs.scrollHeight;
|
||||||
|
logCount++;
|
||||||
|
document.getElementById('log-count').textContent = logCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Seed inicial ───────────────────────────────────────────────────
|
||||||
|
function seed() {
|
||||||
|
if (load().length > 0) return;
|
||||||
|
const seeds = [
|
||||||
|
{ title:'Hot reload Vite + React', type:'feature', priority:'high', status:'todo', domain:'frontend' },
|
||||||
|
{ title:'Backend HMR com tsx watch', type:'feature', priority:'high', status:'doing', domain:'backend' },
|
||||||
|
{ title:'Log stream agregado (Loki)', type:'devops', priority:'medium', status:'todo', domain:'devops' },
|
||||||
|
{ title:'Worker Python para ETL async', type:'feature', priority:'medium', status:'todo', domain:'backend' },
|
||||||
|
{ title:'Test E2E pipeline completo', type:'test', priority:'medium', status:'done', domain:'fullstack' },
|
||||||
|
{ title:'TaskBoard UI (React + Redis)', type:'feature', priority:'high', status:'doing', domain:'frontend' },
|
||||||
|
{ title:'Fix Caddy labels Duplicadas', type:'bug', priority:'low', status:'done', domain:'devops' },
|
||||||
|
{ title:'Migrar monitoring Prometheus', type:'devops', priority:'low', status:'todo', domain:'devops' },
|
||||||
|
];
|
||||||
|
const tasks = seeds.map((s, i) => ({
|
||||||
|
id: genId(), title: s.title, type: s.type, priority: s.priority,
|
||||||
|
status: s.status, domain: s.domain, assignee: null,
|
||||||
|
created_at: new Date(Date.now() - i * 3600000).toISOString(),
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
}));
|
||||||
|
save(tasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
seed();
|
||||||
|
emitChange();
|
||||||
|
addLog('AGENT', 'TaskBoard iniciado — 3 agentes ativos');
|
||||||
|
addLog('INFO', 'Redis: tentando conexão...');
|
||||||
|
try { connectRedis(); } catch(e) { useRedis = false; }
|
||||||
|
if (ws) {
|
||||||
|
ws.onopen = () => { useRedis = true; addLog('AGENT','Redis conectado'); document.querySelector('#redis-status b').style.color='#22c55e'; document.querySelector('#redis-status b').textContent='online'; };
|
||||||
|
ws.onclose = () => { useRedis = false; addLog('WARN','Redis desconectado'); document.querySelector('#redis-status b').style.color='#ef4444'; document.querySelector('#redis-status b').textContent='offline'; };
|
||||||
|
ws.onerror = () => { useRedis = false; document.querySelector('#redis-status b').style.color='#ef4444'; };
|
||||||
|
ws.onmessage = (e) => {
|
||||||
|
try {
|
||||||
|
const msg = JSON.parse(e.data);
|
||||||
|
if (msg.type === 'log') addLog(msg.level || 'INFO', msg.msg);
|
||||||
|
} catch {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name board.octal.tec.br;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
location / { try_files $uri $uri/ /index.html; }
|
||||||
|
add_header X-Content-Type-Options "nosniff";
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// vite.config.ts
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
host: '0.0.0.0',
|
||||||
|
port: 5174,
|
||||||
|
},
|
||||||
|
})
|
||||||
Submodule
+1
Submodule pulse-docs added at e1924ad4f2
Submodule
+1
Submodule pulse-libs added at 29a7a5adb9
Submodule
+1
Submodule pulse-memory added at 73c50dccd5
@@ -0,0 +1,182 @@
|
|||||||
|
import pathlib
|
||||||
|
lib = pathlib.Path("/root/.openclaw/workspace/pulse-libs/src")
|
||||||
|
|
||||||
|
mol = {
|
||||||
|
"FeatureCard.ts": """\
|
||||||
|
import { Card } from '../atoms'
|
||||||
|
interface FeatureCardProps { title:string, description:string, icon?:string, delay?:number, variant?:'default'|'accent', style?:React.CSSProperties }
|
||||||
|
export function FeatureCard({title,description,icon,delay=0,variant='default',style}:FeatureCardProps){
|
||||||
|
const bg = variant==='accent'?'linear-gradient(135deg,rgba(37,99,235,.06),rgba(124,58,237,.04))':'transparent'
|
||||||
|
return (
|
||||||
|
<Card hover style={{cursor:'default',position:'relative',overflow:'hidden',...style}}>
|
||||||
|
{variant==='accent' && <div style={{position:'absolute',inset:0,background:bg,pointerEvents:'none'}}/>}
|
||||||
|
{icon && <div style={{fontSize:'2rem',marginBottom:'.7rem',display:'block'}}>{icon}</div>}
|
||||||
|
<h3 style={{fontSize:'1.05rem',fontWeight:700,marginBottom:'.4rem',color:'#e4e4e7'}}>{title}</h3>
|
||||||
|
<p style={{fontSize:'.85rem',color:'#94a3b8',lineHeight:1.65}}>{description}</p>
|
||||||
|
<div style={{marginTop:'1rem',display:'flex',gap:'.45rem',flexWrap:'wrap'}}>
|
||||||
|
{variant==='accent' && <span style={{padding:'1px 8px',borderRadius:99,background:'rgba(37,99,235,.12)',border:'1px solid rgba(37,99,235,.3)',color:'#60a5fa',fontSize:'.68rem',fontFamily:"'JetBrains Mono',monospace",fontWeight:600}}>core</span>}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
"Navbar.ts": """\
|
||||||
|
import { GradientText, Button } from '../atoms'
|
||||||
|
interface NavProps { logo:string, links:{label:string,href:string}[], ctaLabel?:string, ctaHref?:string }
|
||||||
|
export function Navbar({logo,links,ctaLabel,ctaHref='#'}:NavProps){
|
||||||
|
return (
|
||||||
|
<nav role="navigation" aria-label="Main" style={{position:'sticky',top:0,zIndex:100,display:'flex',alignItems:'center',justifyContent:'space-between',padding:'10px 2rem',background:'rgba(5,5,16,.82)',backdropFilter:'blur(16px)',borderBottom:'1px solid rgba(51,65,85,.3)'}}>
|
||||||
|
<a href="/" aria-label="Home" style={{fontSize:'1rem',fontWeight:900,color:'#e4e4e7',letterSpacing:'-.03em',textDecoration:'none'}}>
|
||||||
|
<GradientText from="#60a5fa" to="#a78bfa">{logo}</GradientText>
|
||||||
|
</a>
|
||||||
|
<ul style={{display:'flex',alignItems:'center',gap:'1.5rem',listStyle:'none',margin:0,padding:0}}>
|
||||||
|
{links.map(l=> <li key={l.href}><a href={l.href} style={{color:'#94a3b8',fontSize:'.88rem',textDecoration:'none',transition:'color .15s'}} onMouseEnter={e=>{e.currentTarget.style.color='#60a5fa'}} onMouseLeave={e=>{e.currentTarget.style.color='#94a3b8'}}>{l.label}</a></li>)}
|
||||||
|
{ctaLabel && <li><Button onClick={()=>{window.location.href=ctaHref}} variant="primary" style={{padding:'6px 16px',fontSize:'.83rem'}}>{ctaLabel}</Button></li>}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
"Footer.ts": """\
|
||||||
|
interface FooterProps { brand?:string, year?:number, links:{label:string,href:string}[] }
|
||||||
|
export function Footer({brand='Pulse 3D',year=__DATE__,links=[]}:FooterProps){
|
||||||
|
return (
|
||||||
|
<footer style={{padding:'1.8rem 2rem',borderTop:'1px solid rgba(51,65,85,.4)',display:'flex',justifyContent:'space-between',alignItems:'center',flexWrap:'wrap',gap:'1rem'}}>
|
||||||
|
<span style={{color:'#64748b',fontSize:'.75rem'}}>{'\\u2699'} {brand} \\u00b7 MIT \\u00b7 {new Date().getFullYear()}</span>
|
||||||
|
<ul style={{display:'flex',gap:'1.2rem',listStyle:'none',margin:0,padding:0}}>
|
||||||
|
{links.map(l=><li key={l.href}><a href={l.href} style={{color:'#60a5fa',fontSize:'.75rem',textDecoration:'none'}} onMouseEnter={e=>{e.currentTarget.style.textDecoration='underline'}} onMouseLeave={e=>{e.currentTarget.style.textDecoration='none'}}>{l.label}</a></li>)}
|
||||||
|
</ul>
|
||||||
|
</footer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
"index.ts": """\
|
||||||
|
export { FeatureCard } from './FeatureCard'
|
||||||
|
export { Navbar } from './Navbar'
|
||||||
|
export { Footer } from './Footer'
|
||||||
|
""",
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, code in mol.items():
|
||||||
|
(lib / "molecules" / name).write_text(code)
|
||||||
|
|
||||||
|
print("✅ Molecules: FeatureCard, Navbar, Footer + index")
|
||||||
|
|
||||||
|
# ── ORGANISMS ──────────────────────────────────────────────────────
|
||||||
|
org = {
|
||||||
|
"HeroSection.ts": """\
|
||||||
|
import { Card, Badge, GradientText, Button } from '../atoms'
|
||||||
|
import type { JSX } from 'react'
|
||||||
|
interface HeroProps { badge?:string, title:string, description:string, cta:{label:string,onClick():void}[], showScrollHint?:boolean, style?:React.CSSProperties }
|
||||||
|
export function HeroSection({badge,title,description,cta=[],showScrollHint=true,style}:HeroProps){
|
||||||
|
return (
|
||||||
|
<section style={{minHeight:'100vh',display:'flex',flexDirection:'column',alignItems:'center',justifyContent:'center',textAlign:'center',padding:'2rem',background:'radial-gradient(ellipse at 50% 20%,rgba(37,99,235,.10) 0%,transparent 65%),radial-gradient(ellipse at 80% 75%,rgba(124,58,237,.07) 0%,transparent 60%)',...style}} role="banner">
|
||||||
|
{badge && <Badge variant="accent">{badge}</Badge>}
|
||||||
|
<h1 style={{fontSize:'clamp(2.2rem,7vw,5.2rem)',fontWeight:900,lineHeight:1.04,letterSpacing:'-.03em',marginBottom:'1rem'}}>
|
||||||
|
<GradientText from="#60a5fa" to="#a78bfa">{title}</GradientText>
|
||||||
|
</h1>
|
||||||
|
<p style={{fontSize:'clamp(.95rem,2.2vw,1.2rem)',color:'#94a3b8',maxWidth:620,marginBottom:'2.5rem',lineHeight:1.6}}>{description}</p>
|
||||||
|
<div style={{display:'flex',gap:'1rem',flexWrap:'wrap',justifyContent:'center'}}>
|
||||||
|
{cta.map((b,i)=><Button key={i} variant={i===0?'primary':'ghost'} onClick={b.onClick}>{b.label}</Button>)}
|
||||||
|
</div>
|
||||||
|
{showScrollHint && <div style={{position:'absolute',bottom:'2.5rem',left:'50%',transform:'translateX(-50%)',color:'#64748b',fontSize:'.72rem',letterSpacing:'.18em',textTransform:'uppercase',display:'flex',flexDirection:'column',alignItems:'center',gap:'.4rem'}}><span>scroll para explorar</span><div style={{animation:'bounce 1.5s infinite'}}>↓</div></div>}
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
"FeaturesGrid.ts": """\
|
||||||
|
import { Card } from '../atoms'
|
||||||
|
interface Feature { icon:string, title:string, description:string }
|
||||||
|
interface FeaturesProps { title?:string, features:Feature[], style?:React.CSSProperties }
|
||||||
|
export function FeaturesGrid({title,features,style}:FeaturesProps){
|
||||||
|
return (
|
||||||
|
<section style={{padding:'3.5rem 2rem',maxWidth:1100,margin:'0 auto',...style}}>
|
||||||
|
{title && <h2 style={{fontSize:'clamp(1.4rem,4vw,2.6rem)',fontWeight:800,lineHeight:1.15,color:'#e4e4e7',marginBottom:'.5rem'}}><span style={{background:'linear-gradient(135deg,#60a5fa,#a78bfa)',WebkitBackgroundClip:'text',WebkitTextFillColor:'transparent'}}>{title}</span></h2>}
|
||||||
|
<p style={{color:'#94a3b8',maxWidth:580,marginBottom:'2rem',fontSize:'.95rem',lineHeight:1.6}}>Componentes que compõem o sistema — do átomo ao organismo.</p>
|
||||||
|
<div style={{display:'grid',gridTemplateColumns:'repeat(auto-fill,minmax(260px,1fr))',gap:'1.2rem'}}>
|
||||||
|
{features.map((f,i)=><Card key={i} hover style={{cursor:'default'}}><div style={{fontSize:'2.2rem',marginBottom:'.8rem'}}>{f.icon}</div><h3 style={{fontSize:'1.04rem',fontWeight:700,marginBottom:'.4rem',color:'#e4e4e7'}}>{f.title}</h3><p style={{fontSize:'.86rem',color:'#94a3b8',lineHeight:1.62}}>{f.description}</p></Card>) }
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
"CtaBlock.ts": """\
|
||||||
|
import { Button, GradientText } from '../atoms'
|
||||||
|
interface CtaProps { title:string, description:string, primary:{label:string,onClick():void}, secondary?:{label:string,onClick():void}, dark?:boolean }
|
||||||
|
export function CtaBlock({title,description,primary,secondary,dark=false}:CtaProps){
|
||||||
|
const bg = dark ? 'rgba(5,5,16,.96)' : 'rgba(15,17,23,.5)'
|
||||||
|
return (
|
||||||
|
<section style={{padding:'6rem 2rem',textAlign:'center',background:'linear-gradient(135deg,rgba(37,99,235,.05),rgba(124,58,237,.05))',borderTop:'1px solid rgba(51,65,85,.3)'}}>
|
||||||
|
<h2 style={{fontSize:'clamp(1.6rem,5vw,3rem)',fontWeight:900,marginBottom:'.8rem',lineHeight:1.15}}><GradientText from="#60a5fa" to="#a78bfa">{title}</GradientText></h2>
|
||||||
|
<p style={{color:'#94a3b8',maxWidth:520,margin:'0 auto 2rem',fontSize:'1.05rem',lineHeight:1.65}}>{description}</p>
|
||||||
|
<div style={{display:'flex',gap:'1rem',flexWrap:'wrap',justifyContent:'center'}}>
|
||||||
|
<Button variant="primary" onClick={primary.onClick} style={{boxShadow:'0 0 40px rgba(37,99,235,.3)'}}>{primary.label}</Button>
|
||||||
|
{secondary && <Button variant="ghost" onClick={()=>window.location.href='/'} style={{border:'1px solid #334155',color:'#e4e4e7'}}>{secondary.label}</Button>}
|
||||||
|
</div>
|
||||||
|
<p style={{marginTop:'2rem',fontSize:'.72rem',color:'#475569'}}>{'\\u2699'} @pulse-libs/ui — MIT — {new Date().getFullYear()}</p>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
"StatsGrid.ts": """\
|
||||||
|
interface Stat { label:string, value:string|number, color?:string }
|
||||||
|
export function StatsGrid({stats}:{stats:Stat[]}){
|
||||||
|
return (
|
||||||
|
<div style={{padding:'3rem 2rem',maxWidth:900,margin:'0 auto',display:'grid',gridTemplateColumns:'repeat(auto-fit,minmax(160px,1fr))',gap:'1rem',textAlign:'center'}}>
|
||||||
|
{stats.map((s,i)=><div key={i} style={{padding:'1.5rem',background:'rgba(15,17,23,.6)',border:'1px solid rgba(51,65,85,.4)',borderRadius:16,backdropFilter:'blur(12px)'}}>
|
||||||
|
<div style={{fontSize:'2rem',fontWeight:900,color:s.color||'#60a5fa'}}>{s.value}</div>
|
||||||
|
<div style={{fontSize:'.8rem',color:'#94a3b8',marginTop:'.2rem'}}>{s.label}</div>
|
||||||
|
</div>)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
"index.ts": """\
|
||||||
|
export { HeroSection } from './HeroSection'
|
||||||
|
export { FeaturesGrid } from './FeaturesGrid'
|
||||||
|
export { CtaBlock } from './CtaBlock'
|
||||||
|
export { StatsGrid } from './StatsGrid'
|
||||||
|
""",
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, code in org.items():
|
||||||
|
if name != "index.ts":
|
||||||
|
(lib / "organisms" / name).write_text(code)
|
||||||
|
else:
|
||||||
|
(lib / "organisms" / name).write_text(code)
|
||||||
|
|
||||||
|
print("✅ Organisms: HeroSection, FeaturesGrid, CtaBlock, StatsGrid + index")
|
||||||
|
|
||||||
|
# ── TEMPLATES ──────────────────────────────────────────────────────
|
||||||
|
templates = {
|
||||||
|
"MainLayout.ts": """\
|
||||||
|
import { ReactNode } from 'react'
|
||||||
|
import { Navbar, Footer, Divider } from './..'
|
||||||
|
interface LayoutProps { children:ReactNode, nav:{logo:string,links:{label:string,href:string}[],cta?:{label:string,href:string}}, footer?:{brand:string,links:{label:string,href:string}[]} }
|
||||||
|
export function MainLayout({children,nav,footer}:LayoutProps){
|
||||||
|
return (
|
||||||
|
<div style={{minHeight:'100vh',display:'flex',flexDirection:'column',background:'#050510'}}>
|
||||||
|
<Navbar logo={nav.logo} links={nav.links} ctaLabel={nav.cta?.label} ctaHref={nav.cta?.href}/>
|
||||||
|
<main style={{flex:1}}>{children}</main>
|
||||||
|
{footer && <Footer brand={footer.brand} links={footer.links}/>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
"MinimalLayout.ts": """\
|
||||||
|
interface MinimalProps { children:React.ReactNode, centered?:boolean }
|
||||||
|
export function MinimalLayout({children,centered=true}:MinimalProps){
|
||||||
|
return <div style={{minHeight:'100vh',display:'flex',alignItems:centered?'center':'flex-start',justifyContent:'center',padding:'4rem 2rem',background:'#050510',color:'#e4e4e7'}}>{children}</div>
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
"index.ts": """\
|
||||||
|
export { MainLayout } from './MainLayout'
|
||||||
|
export { MinimalLayout } from './MinimalLayout'
|
||||||
|
""",
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, code in templates.items():
|
||||||
|
(lib / "templates" / name).write_text(code)
|
||||||
|
|
||||||
|
print("✅ Templates: MainLayout, MinimalLayout + index")
|
||||||
|
PYEOF
|
||||||
@@ -3,5 +3,5 @@
|
|||||||
"registry": "https://clawhub.ai",
|
"registry": "https://clawhub.ai",
|
||||||
"slug": "agent-browser-clawdbot",
|
"slug": "agent-browser-clawdbot",
|
||||||
"installedVersion": "0.1.0",
|
"installedVersion": "0.1.0",
|
||||||
"installedAt": 1779243267499
|
"installedAt": 1779300377917
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,5 +3,5 @@
|
|||||||
"registry": "https://clawhub.ai",
|
"registry": "https://clawhub.ai",
|
||||||
"slug": "multi-search-engine-2-0-1",
|
"slug": "multi-search-engine-2-0-1",
|
||||||
"installedVersion": "1.0.0",
|
"installedVersion": "1.0.0",
|
||||||
"installedAt": 1779243337647
|
"installedAt": 1779300386960
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,5 @@
|
|||||||
"registry": "https://clawhub.ai",
|
"registry": "https://clawhub.ai",
|
||||||
"slug": "nova-self-improver",
|
"slug": "nova-self-improver",
|
||||||
"installedVersion": "1.0.0",
|
"installedVersion": "1.0.0",
|
||||||
"installedAt": 1779233727231,
|
"installedAt": 1779300391129
|
||||||
"fingerprint": "78374b4453949014f0e21d2586fdcbe6e159cd0ee437cea005b8ec400e4185ec"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,5 +3,5 @@
|
|||||||
"registry": "https://clawhub.ai",
|
"registry": "https://clawhub.ai",
|
||||||
"slug": "redis-labs-integration",
|
"slug": "redis-labs-integration",
|
||||||
"installedVersion": "1.0.2",
|
"installedVersion": "1.0.2",
|
||||||
"installedAt": 1779243351883
|
"installedAt": 1779300388864
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,5 +3,5 @@
|
|||||||
"registry": "https://clawhub.ai",
|
"registry": "https://clawhub.ai",
|
||||||
"slug": "self-improvement",
|
"slug": "self-improvement",
|
||||||
"installedVersion": "1.0.0",
|
"installedVersion": "1.0.0",
|
||||||
"installedAt": 1779243314748
|
"installedAt": 1779300383146
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,5 @@
|
|||||||
"registry": "https://clawhub.ai",
|
"registry": "https://clawhub.ai",
|
||||||
"slug": "typescript",
|
"slug": "typescript",
|
||||||
"installedVersion": "1.0.2",
|
"installedVersion": "1.0.2",
|
||||||
"installedAt": 1779234199574,
|
"installedAt": 1779300393477
|
||||||
"fingerprint": "9c948b42fc4b93fa1062c3e16d1a28715b78afc4860ef5418947c86d8b72253d"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,5 +3,5 @@
|
|||||||
"registry": "https://clawhub.ai",
|
"registry": "https://clawhub.ai",
|
||||||
"slug": "vision",
|
"slug": "vision",
|
||||||
"installedVersion": "3.5.0",
|
"installedVersion": "3.5.0",
|
||||||
"installedAt": 1779243291577
|
"installedAt": 1779300380127
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user