feat(design-system): DESIGN_SYSTEM.md + prompts + extract-design command

- frontend/DESIGN_SYSTEM.md: contrato del @fn_library (regla suprema para
  Claude Design y agentes).
- frontend/design_prompts/: 11 plantillas de prompt (onboarding, dashboard,
  crud, detail, settings, auth, error, custom, handoff_integrate) +
  questionnaire numerado para arranque rapido.
- .claude/commands/extract-design.md: workflow de 10 pasos para extraer
  componentes nuevos y mejoras desde exports "standalone" de Claude Design
  al registry, sync al espejo fn-design-system y push a gitea+github.
- .claude/scripts/extract_design_bundle.py: decodificador del bundle
  (base64+gzip en manifest, nombra JSX por heuristica de header).
- .gitignore: ignorar subrepos/*/ (el mirror fn-design-system es repo
  propio con remotes dataforge/fn-design-system + gutierenmanuel/fn-design-system).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-21 20:46:00 +02:00
parent d771c21a46
commit ca07927d38
15 changed files with 1587 additions and 0 deletions
+281
View File
@@ -0,0 +1,281 @@
# /extract-design — Mejorar @fn_library con exports de Claude Design
Eres un agente mejorador del design system. Tu trabajo es analizar un export "standalone" de Claude Design (`sources/frontend_designs/*.html`), identificar componentes nuevos o mejoras sobre `@fn_library`, aplicarlos al registry y propagarlos al espejo público `subrepos/fn-design-system` (GitHub + Gitea).
**Objetivo:** cada diseño exportado debería dejar el registry un poco mejor que antes. Lo que Claude Design inventó para cubrir un hueco hoy → componente reutilizable del registry mañana.
---
## Argumento
`$ARGUMENTS` — ruta al `.html` en `sources/frontend_designs/`. Si no se proporciona:
1. Lista los `.html` bajo `sources/frontend_designs/` ordenados por fecha.
2. Muestra fecha + nombre + tamaño.
3. Pregunta cuál procesar. Default: el más reciente.
---
## PASO 0 — Validar input
```bash
ls -lht sources/frontend_designs/*.html 2>/dev/null
```
Si no existe el fichero, abortar. Si existe, leer las primeras líneas para confirmar que es un export de Claude Design (`__bundler/manifest`, `__bundler/template` en el HTML).
---
## PASO 1 — Decodificar el bundle
Ejecutar el extractor:
```bash
python3 .claude/scripts/extract_design_bundle.py \
"sources/frontend_designs/<NOMBRE>.html" \
"sources/frontend_designs/<NOMBRE>_extracted/"
```
Esperado: directorio con `app.jsx`, `fn_library_emu.jsx`, `charts_emu.jsx`, `data.jsx` + fuentes woff2 + `manifest.json`.
Si falta alguno de los 4 `.jsx` clave, inspeccionar por UUID; puede que Claude Design haya usado estructura distinta. Reportar al usuario.
---
## PASO 2 — Inventariar el diseño
Leer `app.jsx` y listar **todos los componentes React definidos** (funciones que empiezan con mayúscula o usan `function Xxx(`). Categorizar:
### 2a. Componentes del export que YA existen en `@fn_library`
- Grep el barrel: `cat frontend/functions/ui/index.ts | grep "^export"`.
- Para cada componente del export, ver si aparece en el barrel. Registrar coincidencias.
### 2b. Componentes nuevos (no existen en el registry)
Componentes React del `app.jsx` cuyo nombre no aparece en el barrel. Estos son **candidatos a extracción**.
### 2c. Uso de variantes / props no documentadas
Leer `fn_library_emu.jsx` del export y comparar API con tus `.tsx` reales:
```bash
# Comprobar componentes específicos si el export los usa con props nuevas
sqlite3 registry.db "SELECT id, signature, props FROM functions WHERE id = 'alert_ts_ui';"
```
Anotar discrepancias (variantes faltantes, props nuevas, tipos distintos).
### 2d. Datos/patrones reutilizables en `data.jsx`
- RNG determinista (mulberry32) → candidato a `frontend/functions/core/rng_seeded_ts_core` o `python/functions/core/`.
- Helpers tipo `statusBadge()` → documentar como receta, no como componente.
---
## PASO 3 — Consultar el registry para evitar duplicados
Para cada componente candidato del paso 2b, búsqueda FTS5 antes de proponerlo:
```bash
sqlite3 registry.db "SELECT id, kind, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:<CANDIDATO>* OR description:<PALABRAS_CLAVE>') ORDER BY name;"
```
Si encuentras algo similar que pueda ser mejorado en lugar de duplicado, márcalo como **mejora** a ese existente.
---
## PASO 4 — Presentar el diagnóstico al usuario
Muestra en tablas separadas:
### 🟢 Componentes nuevos candidatos
| # | Nombre propuesto | Dominio | Líneas | Reutilizable en | API |
|---|---|---|---|---|---|
| 1 | `funnel_chart_ts_ui` | ui | ~35 | CRM, analytics, funnels genéricos | `(data: Array<{stage, value}>, variant?) → JSX` |
### 🟡 Mejoras a componentes existentes
| # | Componente | Mejora | Tipo | Riesgo |
|---|---|---|---|---|
| A | `alert_ts_ui` | Añadir variantes `success`, `warning`, `info` | Expandir enum | Bajo — no rompe API |
| B | `data_table_ts_ui` | Prop `density: 'compact'|'cozy'|'roomy'` | Añadir prop opcional | Bajo |
### 🔵 Patrones a documentar (no componente)
| Patrón | Dónde registrar |
|---|---|
| `statusBadge` helper | `DESIGN_SYSTEM.md` sección "patterns" |
**Esperar confirmación.** El usuario responde con sintaxis `1,2,A,B` (o `all`, o `nuevos only`, o descarta algunos). Si dice `all`, aplica todo lo listado.
---
## PASO 5 — Aplicar mejoras aprobadas
### 5a. Para componentes nuevos (candidatos 🟢)
Por cada aprobado:
1. **Leer código** del `app.jsx` / `fn_library_emu.jsx` / `charts_emu.jsx` del export.
2. **Adaptar al stack real del registry:**
- Cambiar elementos SVG/HTML planos por primitivas de `@mantine/core` cuando corresponda (`Paper`, `Stack`, `Group`, `Text`).
- Cambiar `style={{...}}` por props Mantine (`p`, `m`, `fw`, `gap`, `radius`, `c`).
- Si es un chart, delegar en `@mantine/charts` cuando sea posible; solo usar SVG puro si Mantine no cubre el caso (ej: `Sparkline` en el registry ya es SVG puro por rendimiento).
- Iconos: `@tabler/icons-react`.
3. **Crear los dos ficheros** siguiendo la convención:
- `frontend/functions/ui/<name>.tsx` — código React.
- `frontend/functions/ui/<name>.md` — frontmatter completo.
4. **Frontmatter del .md** (campos clave):
```yaml
id: <name>_ts_ui
name: <name>
kind: component
lang: ts
domain: ui
purity: impure
framework: react
version: 1.0.0
description: "..."
tags: [...]
props: {...}
emits: null
params: []
output: "JSX.Element — ..."
source_repo: "claude.ai/design"
source_license: ""
source_file: "sources/frontend_designs/<NOMBRE>.html"
file_path: frontend/functions/ui/<name>.tsx
tested: false
```
5. **Añadir al barrel** `frontend/functions/ui/index.ts`: `export { Xxx } from './<name>'`.
### 5b. Para mejoras a componentes existentes (🟡)
Por cada aprobada:
1. **Leer** el `.tsx` actual.
2. **Aplicar la mejora** sin romper la API existente: añade prop opcional, amplía enum de `variant`, etc.
3. **Actualizar** el `.md` correspondiente para reflejar las nuevas variantes/props (campos `variant`, `props`, `description`).
4. **Si la firma cambia**, actualizar también el `signature` del frontmatter.
### 5c. Para patrones a documentar (🔵)
1. Añadir una sección "Patterns" en `frontend/DESIGN_SYSTEM.md` si no existe.
2. Registrar el patrón con un ejemplo corto.
---
## PASO 6 — Indexar y verificar
```bash
./fn index
```
- Si falla por integridad, arreglar y reintentar.
- Verificar cada componente nuevo: `./fn show <id>`.
- Confirmar que el barrel compila haciendo `cd frontend && pnpm tsc --noEmit` (si tarda, al menos verificar imports manualmente).
---
## PASO 7 — Sincronizar al espejo
```bash
cd subrepos/fn-design-system
./sync_from_registry.sh
git add -A
git status --short # Mostrar qué cambió en el espejo
```
Si hay cambios, preparar commit. Si no, el sync no recogió las modificaciones — investigar.
---
## PASO 8 — Commit en ambos repos
### 8a. Commit en `fn_registry`
```bash
git add frontend/functions/ui/ frontend/DESIGN_SYSTEM.md registry.db
git commit -m "$(cat <<'EOF'
feat(ui): extract <N> components / <M> improvements from design export
From: sources/frontend_designs/<NOMBRE>.html
New components:
- <id> — <descripción corta>
- ...
Improvements:
- <id> — <cambio>
- ...
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```
### 8b. Commit en el espejo
```bash
cd subrepos/fn-design-system
git commit -m "sync: <N> new components + <M> improvements from <NOMBRE>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
```
---
## PASO 9 — Push
### 9a. Push del espejo (ambos remotes)
```bash
cd subrepos/fn-design-system
./push_all.sh
```
Esto propaga a:
- `gitea/dataforge/fn-design-system`
- `github/gutierenmanuel/fn-design-system` ← este es el que Claude Design consume
### 9b. Push de fn_registry
**Preguntar al usuario** antes — no push sin permiso (ver CLAUDE.md del proyecto). Si dice sí:
```bash
git push origin master
```
---
## PASO 10 — Resumen final
Mostrar al usuario:
```
✓ Extracción completa.
Nuevos componentes en @fn_library:
- <id> (frontend/functions/ui/<name>.tsx)
- ...
Mejoras aplicadas:
- <id>: <qué cambió>
Espejo actualizado:
- Commit gitea: <sha> → <url>
- Commit github: <sha> → <url>
Claude Design verá estas mejoras en su próxima lectura del repo enlazado.
Siguiente acción sugerida: probar un prompt de dashboard que use <componente_nuevo>.
```
---
## Reglas críticas
- **NUNCA extraer sin aprobación explícita del usuario** — siempre paso 4 con tabla y espera.
- **NUNCA sobrescribir un componente existente** en el paso 5b — solo añadir variantes/props opcionales. Si la mejora es incompatible, proponerlo como propuesta aparte (`fn proposal add`) en vez de aplicarla.
- **SIEMPRE `source_repo: "claude.ai/design"`** en el frontmatter de componentes nuevos, y `source_file` apuntando al `.html` original.
- **SIEMPRE mantener el orden:** registry → index → verify → sync mirror → commit both → push mirror → (ask to push fn_registry).
- **El barrel `index.ts`** debe estar actualizado antes de hacer `fn index` (hay apps que lo importan).
- **NO committear** `operations.db*`, `node_modules/`, `dist/`, `.env` ni nada que `.gitignore` excluya. Usa `git add` con rutas explícitas, no `git add -A` a ciegas.
- **Si el usuario cancela a mitad**, dejar el working tree limpio o documentar qué quedó pendiente. No medio-commits.
- **Patrones que no tienen sentido como primitiva** (ej. envs, branding específico) → documentar, no componentizar.
+159
View File
@@ -0,0 +1,159 @@
#!/usr/bin/env python3
"""
Extract Claude Design "standalone" HTML exports.
Claude Design packs the whole React app as base64+gzip blobs inside
<script type="__bundler/manifest"> tags. This script decompresses them
and writes each asset (JSX, CSS, fonts) to a target directory.
Usage:
python3 extract_design_bundle.py <path/to/export.html> <output_dir>
The output dir will contain:
data.jsx (if detected by header comment)
fn_library_emu.jsx (lib emulation)
charts_emu.jsx (charts emulation)
app.jsx (main tree)
<uuid>.<ext> (anything else — fonts, unknown js)
manifest.json (summary of all assets: uuid, mime, bytes, filename)
JSX files are named heuristically from their leading comment. If names
cannot be inferred from headers, they keep their uuid prefix.
"""
from __future__ import annotations
import base64
import gzip
import json
import pathlib
import re
import sys
MIME_TO_EXT = {
"text/javascript": "js",
"application/javascript": "js",
"text/babel": "jsx",
"application/json": "json",
"text/css": "css",
"image/svg+xml": "svg",
"font/woff2": "woff2",
"font/woff": "woff",
"text/html": "html",
}
# Order matters: first matching hint wins. Put MORE SPECIFIC patterns first.
HEADER_HINTS = [
("charts_emu.jsx", [r"Emulaci(ó|o)n de @fn_library/\{", r"LineChart, AreaChart, BarChart"]),
("fn_library_emu.jsx", [r"Emulaci(ó|o)n visual de @fn_library"]),
("data.jsx", [r"mock data \(determinista\)", r"window\.\w+Data\s*="]),
("app.jsx", [r"ReactDOM\.createRoot", r"arbol principal", r"function App\s*\("]),
]
def pick_name(content: str, used_names: set[str]) -> str | None:
head = content[:2000]
for name, patterns in HEADER_HINTS:
if name in used_names:
continue
if any(re.search(p, head, re.IGNORECASE) for p in patterns):
return name
return None
def grab_script(html: str, kind: str) -> str | None:
m = re.search(
r'<script type="__bundler/' + kind + r'">\s*(.*?)\s*</script>',
html, re.DOTALL,
)
return m.group(1) if m else None
def extract(html_path: pathlib.Path, out_dir: pathlib.Path) -> dict:
html = html_path.read_text(encoding="utf-8")
manifest_raw = grab_script(html, "manifest")
if not manifest_raw:
raise SystemExit(f"No <script type='__bundler/manifest'> found in {html_path}")
manifest = json.loads(manifest_raw)
ext_raw = grab_script(html, "ext_resources")
ext_resources = json.loads(ext_raw) if ext_raw else []
id_map = {e["uuid"]: e.get("id", e["uuid"]) for e in ext_resources}
out_dir.mkdir(parents=True, exist_ok=True)
summary = []
used_names: set[str] = set()
# First pass: decode all assets and collect jsx blobs (so we can name them by header hint)
decoded: list[tuple[str, str, bytes]] = [] # (uuid, mime, bytes)
for uuid, entry in manifest.items():
raw = base64.b64decode(entry["data"])
if entry.get("compressed"):
raw = gzip.decompress(raw)
decoded.append((uuid, entry.get("mime", "application/octet-stream"), raw))
# Second pass: write files with heuristic names for known jsx
for uuid, mime, raw in decoded:
ext = MIME_TO_EXT.get(mime, "bin")
filename = None
# Heuristic for JSX / JS that represents the app
if ext in ("jsx", "js"):
try:
text = raw.decode("utf-8", errors="replace")
name = pick_name(text, used_names)
if name:
filename = name
used_names.add(name)
except Exception:
pass
if not filename:
# Fall back to ext_resources id if present, or uuid
base = id_map.get(uuid, uuid)
safe = re.sub(r"[^A-Za-z0-9._-]", "_", base)[:80]
filename = f"{safe}.{ext}"
path = out_dir / filename
# Avoid collisions
i = 2
while path.exists():
stem = path.stem
path = out_dir / f"{stem}_{i}.{ext}"
i += 1
path.write_bytes(raw)
summary.append({
"uuid": uuid,
"mime": mime,
"bytes": len(raw),
"filename": path.name,
})
(out_dir / "manifest.json").write_text(
json.dumps({"source": str(html_path), "assets": summary}, indent=2),
encoding="utf-8",
)
return {"assets": summary, "out": str(out_dir)}
def main():
if len(sys.argv) < 3:
print(__doc__)
sys.exit(2)
html = pathlib.Path(sys.argv[1])
out = pathlib.Path(sys.argv[2])
if not html.exists():
sys.exit(f"Input not found: {html}")
result = extract(html, out)
print(f"✓ Extracted {len(result['assets'])} assets to {result['out']}")
print(f" Manifest: {out}/manifest.json")
print()
# Short preview per asset
for a in result["assets"]:
print(f" {a['mime']:28s} {a['bytes']:>8} B {a['filename']}")
if __name__ == "__main__":
main()