arckode-ui 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/skills/SKILL.md +866 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arckode-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Frontend framework con .ark SFCs, signals, file-system router y analyzer estático con sugerencias concretas de fix. Diseñado para máxima predictibilidad de output de IA.",
|
|
6
6
|
"keywords": [
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"main": "./dist/index.js",
|
|
39
39
|
"types": "./dist/index.d.ts",
|
|
40
40
|
"bin": {
|
|
41
|
-
"ark": "
|
|
41
|
+
"ark": "dist/cli.js"
|
|
42
42
|
},
|
|
43
43
|
"files": [
|
|
44
44
|
"dist",
|
package/skills/SKILL.md
ADDED
|
@@ -0,0 +1,866 @@
|
|
|
1
|
+
# SKILL: Arckode UI — AI Development Protocol
|
|
2
|
+
|
|
3
|
+
> Leé esto COMPLETO antes de escribir una sola línea de `.ark` o de código que use `arckode-ui`.
|
|
4
|
+
> Este skill se activa cuando: hay archivos `.ark`, se importa de `arckode-ui` o `arckode-ui/vite`,
|
|
5
|
+
> se usa `defineComponent`, `signal`, `computed`, `defineStore`, `createService`, `ark analyze`, `ark generate`.
|
|
6
|
+
|
|
7
|
+
## SUB-SKILLS DISPONIBLES (publicados con el paquete npm)
|
|
8
|
+
|
|
9
|
+
Cuando necesités detalle profundo de un área, leer el sub-skill correspondiente.
|
|
10
|
+
Los sub-skills están en `./node_modules/arckode-ui/skills/` (si el proyecto usa npm/bun install):
|
|
11
|
+
|
|
12
|
+
| Contexto | Sub-skill a leer |
|
|
13
|
+
|----------|------------------|
|
|
14
|
+
| Escribir un `.ark` para usuario, patrones de UI (modal con slot, form con validación, lista con curried action, `:class` reactivo) | `./node_modules/arckode-ui/skills/components/SKILL.md` |
|
|
15
|
+
| Signals, computed, watch, effect, lifecycle, renderer, comunicación padre↔hijo | `./node_modules/arckode-ui/skills/runtime/SKILL.md` |
|
|
16
|
+
| Parser, template-compiler, vite-plugin, agregar directiva nueva | `./node_modules/arckode-ui/skills/compiler/SKILL.md` |
|
|
17
|
+
| Agregar regla nueva al analyzer, fix suggestions, mensajes | `./node_modules/arckode-ui/skills/analyzer/SKILL.md` |
|
|
18
|
+
| CLI `ark`, plantillas del generator, build del bin | `./node_modules/arckode-ui/skills/cli/SKILL.md` |
|
|
19
|
+
| Vitest + happy-dom, mockear fetch, testar componentes | `./node_modules/arckode-ui/skills/testing/SKILL.md` |
|
|
20
|
+
|
|
21
|
+
> Si el proyecto está desarrollando el framework (no usándolo como dep), los sub-skills están en `./skills/` directamente.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 1. QUÉ ES ARCKODE UI (entiendelo ANTES de codear)
|
|
26
|
+
|
|
27
|
+
Framework frontend **architected FOR AI** — su objetivo no es ser flexible sino **predecible**.
|
|
28
|
+
Un componente `.ark` SIEMPRE tiene la misma estructura. El analizador detecta cuando la rompés
|
|
29
|
+
y SIEMPRE sugiere el fix concreto en el campo `violation.fix`.
|
|
30
|
+
|
|
31
|
+
**Pila obligatoria:**
|
|
32
|
+
- TypeScript strict (no `any`)
|
|
33
|
+
- Vite con `arkcodeUi()` plugin
|
|
34
|
+
- Signals (no ref/reactive)
|
|
35
|
+
- HTML declarativo compilado (no JSX)
|
|
36
|
+
- Bun (recomendado) o Node ≥ 18
|
|
37
|
+
|
|
38
|
+
**El composition root es `main.ts` + el `App.ark` root.** Si un componente no está importado en alguna chain desde App, no existe.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## 2. PROTOCOLO OBLIGATORIO (antes de escribir código)
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
PASO 1 — IDENTIFICAR ALCANCE
|
|
46
|
+
¿La tarea toca 1 componente nuevo? → Seguir
|
|
47
|
+
¿La tarea toca 1 componente existente? → Leer el .ark primero, no asumir
|
|
48
|
+
¿La tarea toca varios componentes hermanos? → Identificar si necesita un store
|
|
49
|
+
¿La tarea toca el routing? → Trabajar en src/pages/
|
|
50
|
+
¿La tarea toca HTTP a un backend? → Crear/modificar service, NO tocar componente
|
|
51
|
+
¿La tarea toca estado global compartido? → defineStore en src/stores/
|
|
52
|
+
¿La tarea toca el framework en sí? → DETENERSE. Ese es trabajo del repo del framework.
|
|
53
|
+
|
|
54
|
+
PASO 2 — VERIFICAR REGLAS (ver §3)
|
|
55
|
+
¿Estoy poniendo lógica en template? → REGLA #11 — extraer a computed/action
|
|
56
|
+
¿Estoy usando ref()/reactive()? → REGLA #8 — usar signal()
|
|
57
|
+
¿Estoy haciendo fetch() en el componente? → REGLA #10 — mover a service
|
|
58
|
+
¿Estoy usando provide/inject/v-model? → NO EXISTEN. Reglas #7 y #9
|
|
59
|
+
¿Mi handler @click no empieza con actions.? → REGLA #4
|
|
60
|
+
¿Mi v-for tiene colección sin namespace? → REGLA #5
|
|
61
|
+
¿Mi return de setup tiene llaves extra? → REGLA #3
|
|
62
|
+
¿Mi prop no tiene type? → REGLA #1
|
|
63
|
+
¿Mi emit usa camelCase? → REGLA #2
|
|
64
|
+
|
|
65
|
+
PASO 3 — GENERAR
|
|
66
|
+
Si es componente nuevo: copiar la PLANTILLA CANÓNICA (§5) y adaptar
|
|
67
|
+
Si es modificación: respetar el orden del setup() (§4)
|
|
68
|
+
Si es estado global: copiar PATRÓN DE STORE (§7)
|
|
69
|
+
Si es service HTTP: copiar PATRÓN DE SERVICE (§8)
|
|
70
|
+
|
|
71
|
+
PASO 4 — VERIFICAR (NO ASUMIR que funciona)
|
|
72
|
+
npx ark analyze → 0 errors. Si hay errors, leer violation.fix y aplicar
|
|
73
|
+
bun test → si trabajás en el framework
|
|
74
|
+
Probar en navegador → npx vite (o bun dev) y verificar que la feature funciona
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## 3. REGLAS INMUTABLES (no negociables — el analyzer las detecta)
|
|
80
|
+
|
|
81
|
+
Cada regla tiene un código que aparece en el output del analyzer. La IA puede referenciarlas
|
|
82
|
+
diciendo "esto viola REGLA #N" para ser explícita.
|
|
83
|
+
|
|
84
|
+
### ⚠ REGLA #1 — Toda prop DEBE tener `type` (clase nativa)
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
// ❌
|
|
88
|
+
props: { name: { required: true } } // PROP_MISSING_TYPE
|
|
89
|
+
props: { name: { type: 'string' } } // string en vez de String
|
|
90
|
+
|
|
91
|
+
// ✅
|
|
92
|
+
props: { name: { type: String, required: true } }
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Tipos válidos: `String | Number | Boolean | Array | Object | Function` (clases nativas).
|
|
96
|
+
|
|
97
|
+
### ⚠ REGLA #2 — `emits` SIEMPRE en kebab-case
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
// ❌
|
|
101
|
+
emits: ['userSaved', 'formCancelled'] // EMIT_CAMELCASE
|
|
102
|
+
|
|
103
|
+
// ✅
|
|
104
|
+
emits: ['user-saved', 'form-cancelled']
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Al escuchar: `<Child @user-saved="actions.x">`. Al emitir: `emit('user-saved', payload)`.
|
|
108
|
+
|
|
109
|
+
### ⚠ REGLA #3 — `setup()` retorna SOLO `state`, `computed`, `actions`
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
// ❌
|
|
113
|
+
return { state: {}, computed: {}, actions: {}, methods: {} } // SETUP_UNKNOWN_RETURN_KEY
|
|
114
|
+
return { signals: {}, ... } // signals no es válido
|
|
115
|
+
|
|
116
|
+
// ✅
|
|
117
|
+
return { state, computed, actions }
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Llaves OK: `state` (signals mutables), `computed` (derivados readonly), `actions` (functions).
|
|
121
|
+
NO se permite otra. Si tu return tiene una llave extra, mover su contenido a una de las 3.
|
|
122
|
+
|
|
123
|
+
### ⚠ REGLA #4 — Handlers de eventos SIEMPRE `actions.xxx`
|
|
124
|
+
|
|
125
|
+
```html
|
|
126
|
+
<!-- ❌ -->
|
|
127
|
+
<button @click="handleClick"> <!-- HANDLER_NOT_IN_ACTIONS -->
|
|
128
|
+
<button @click="state.count.value++"> <!-- LOGIC_IN_TEMPLATE + HANDLER_NOT_IN_ACTIONS -->
|
|
129
|
+
<button @click="submit()">
|
|
130
|
+
|
|
131
|
+
<!-- ✅ -->
|
|
132
|
+
<button @click="actions.handleClick">
|
|
133
|
+
<button @click="actions.increment">
|
|
134
|
+
<button @click="actions.toggleTodo(todo.id)"> <!-- curried OK -->
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### ⚠ REGLA #5 — `v-for` colección SIEMPRE namespaced
|
|
138
|
+
|
|
139
|
+
```html
|
|
140
|
+
<!-- ❌ -->
|
|
141
|
+
<li v-for="item in items"> <!-- VFOR_NOT_NAMESPACED -->
|
|
142
|
+
<li v-for="x in getList()"> <!-- llamada a función -->
|
|
143
|
+
|
|
144
|
+
<!-- ✅ -->
|
|
145
|
+
<li v-for="item in state.items.value">
|
|
146
|
+
<li v-for="tab in computed.tabs.value">
|
|
147
|
+
<li v-for="opt in props.options">
|
|
148
|
+
<li v-for="(item, idx) in state.items.value"> <!-- con índice -->
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### ⚠ REGLA #6 — `v-if` y `v-else-if` deben usar acceso namespaced (con `.`)
|
|
152
|
+
|
|
153
|
+
```html
|
|
154
|
+
<!-- ⚠ warning -->
|
|
155
|
+
<div v-if="visible"> <!-- VIF_NOT_NAMESPACED -->
|
|
156
|
+
|
|
157
|
+
<!-- ✅ -->
|
|
158
|
+
<div v-if="state.visible.value">
|
|
159
|
+
<div v-if="computed.isReady.value">
|
|
160
|
+
<div v-if="props.active">
|
|
161
|
+
<div v-if="todo.done"> <!-- variable de loop con dot -->
|
|
162
|
+
<div v-if="state.count.value > 0">
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### ⚠ REGLA #7 — NO existe `v-model`. Input controlado explícito
|
|
166
|
+
|
|
167
|
+
```html
|
|
168
|
+
<!-- ❌ -->
|
|
169
|
+
<input v-model="state.email" />
|
|
170
|
+
|
|
171
|
+
<!-- ✅ -->
|
|
172
|
+
<input :value="state.email.value" @input="actions.onEmailInput" />
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
function onEmailInput(e: Event) {
|
|
177
|
+
email.value = (e.target as HTMLInputElement).value
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### ⚠ REGLA #8 — NO existe `ref()` ni `reactive()`. Solo `signal`/`computed`
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
// ❌
|
|
185
|
+
const x = ref(0) // REF_REACTIVE_USAGE
|
|
186
|
+
const s = reactive({ a: 1, b: 2 }) // REF_REACTIVE_USAGE
|
|
187
|
+
|
|
188
|
+
// ✅
|
|
189
|
+
const x = signal(0)
|
|
190
|
+
const a = signal(1); const b = signal(2) // un signal por propiedad
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### ⚠ REGLA #9 — NO existe `provide`/`inject`. Estado compartido = `defineStore`
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
// ❌
|
|
197
|
+
provide('key', value) // PROVIDE_INJECT_USAGE
|
|
198
|
+
const v = inject('key')
|
|
199
|
+
|
|
200
|
+
// ✅
|
|
201
|
+
// stores/auth.store.ts
|
|
202
|
+
export const useAuthStore = defineStore('auth', { state, getters, actions })
|
|
203
|
+
// componente
|
|
204
|
+
const auth = useAuthStore()
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### ⚠ REGLA #10 — `fetch()` PROHIBIDO en componente. Siempre via service
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
// ❌
|
|
211
|
+
onMount(() => fetch('/api/users')) // DIRECT_FETCH_IN_COMPONENT
|
|
212
|
+
|
|
213
|
+
// ✅
|
|
214
|
+
// services/user.service.ts
|
|
215
|
+
export const UserService = createService(
|
|
216
|
+
{ baseUrl: '/api/users' },
|
|
217
|
+
{ async getAll() { return this.get<User[]>('/') } },
|
|
218
|
+
)
|
|
219
|
+
// componente
|
|
220
|
+
import { UserService } from '../services/user.service'
|
|
221
|
+
function load() { UserService.getAll().then(...) }
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### ⚠ REGLA #11 — NO lógica en directivas (`++`, `--`, ternario complejo)
|
|
225
|
+
|
|
226
|
+
```html
|
|
227
|
+
<!-- ❌ -->
|
|
228
|
+
<button @click="state.count.value++"> <!-- LOGIC_IN_TEMPLATE -->
|
|
229
|
+
<div :class="state.x.value > 0 ? 'a' : 'b'"> <!-- ternario en :class -->
|
|
230
|
+
|
|
231
|
+
<!-- ✅ -->
|
|
232
|
+
<button @click="actions.increment">
|
|
233
|
+
<div :class="computed.xClass.value"> <!-- mover a computed -->
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### ⚠ REGLA #12 — Actions como `function declaration`, no arrow
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
// ❌
|
|
240
|
+
actions: { submit: () => { ... } } // ARROW_FUNCTION_ACTION
|
|
241
|
+
const submit = () => { ... }
|
|
242
|
+
return { actions: { submit } } // ARROW_FUNCTION_ACTION
|
|
243
|
+
|
|
244
|
+
// ✅
|
|
245
|
+
function submit() { ... }
|
|
246
|
+
return { actions: { submit } }
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### ⚠ REGLA #13 — Componentes hijos: PascalCase + import explícito
|
|
250
|
+
|
|
251
|
+
```html
|
|
252
|
+
<!-- ❌ -->
|
|
253
|
+
<usercard :prop="x" /> <!-- minúscula = elemento HTML -->
|
|
254
|
+
|
|
255
|
+
<!-- ✅ -->
|
|
256
|
+
<UserCard :prop="state.x.value" />
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
// import obligatorio
|
|
261
|
+
import UserCard from './UserCard.ark'
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### ⚠ REGLA #14 — `mount(component, selector)` toma string, no HTMLElement
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
// ❌
|
|
268
|
+
mount(App, document.getElementById('app')!)
|
|
269
|
+
|
|
270
|
+
// ✅
|
|
271
|
+
mount(App, '#app')
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### ⚠ REGLA #15 — Imports SOLO de `'arckode-ui'` o `'arckode-ui/vite'`
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
// ❌
|
|
278
|
+
import { ref } from 'vue'
|
|
279
|
+
import { useState } from 'react'
|
|
280
|
+
import { defineStore } from 'arckode-ui/store' // no existe ese subpath
|
|
281
|
+
|
|
282
|
+
// ✅
|
|
283
|
+
import { defineComponent, signal, computed, defineStore, createService } from 'arckode-ui'
|
|
284
|
+
import { arkcodeUi } from 'arckode-ui/vite'
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Plugin se llama **`arkcodeUi`** (no `arckodeUi`, no `arckodePlugin`).
|
|
288
|
+
|
|
289
|
+
### ⚠ REGLA #16 — Orden recomendado del `setup()` (convención, no enforced)
|
|
290
|
+
|
|
291
|
+
```
|
|
292
|
+
1. signals → const x = signal(...)
|
|
293
|
+
2. computed → const y = computed(...)
|
|
294
|
+
3. lifecycle → onMount(...)
|
|
295
|
+
4. watch → watch(x, ...)
|
|
296
|
+
5. actions → function doSomething() {}
|
|
297
|
+
6. return → return { state, computed, actions }
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
El analyzer NO enforza este orden. Es para legibilidad y para que la IA prediga la posición.
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
## 4. ÁRBOLES DE DECISIÓN (Si te piden X, hacé Y)
|
|
305
|
+
|
|
306
|
+
### ¿Te piden CREAR un componente nuevo?
|
|
307
|
+
|
|
308
|
+
```
|
|
309
|
+
¿Qué tipo de componente?
|
|
310
|
+
|
|
311
|
+
→ Página (URL accesible)
|
|
312
|
+
Comando: ark generate page <kebab/o/[id]>
|
|
313
|
+
Path: src/pages/<name>.ark
|
|
314
|
+
Mountable directamente desde el file-system router
|
|
315
|
+
|
|
316
|
+
→ Layout (wrapper de carpeta)
|
|
317
|
+
Comando: ark generate layout <name>
|
|
318
|
+
Path: src/pages/<name>/_layout.ark
|
|
319
|
+
Tiene <slot /> obligatorio
|
|
320
|
+
|
|
321
|
+
→ Componente de dominio (UserCard, ProductList, etc.)
|
|
322
|
+
Comando: ark generate component <PascalCase>
|
|
323
|
+
Path: src/components/features/<Name>.ark
|
|
324
|
+
|
|
325
|
+
→ Primitivo de UI (Button, Input, Modal)
|
|
326
|
+
Manual: src/components/ui/<Name>.ark
|
|
327
|
+
Igual estructura que componente de dominio
|
|
328
|
+
|
|
329
|
+
→ Wrapper con contenido inyectable
|
|
330
|
+
Manual: usar <slot /> en el template, ver patrón en §6
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### ¿Te piden AGREGAR estado a un componente?
|
|
334
|
+
|
|
335
|
+
```
|
|
336
|
+
¿El dato es local al componente?
|
|
337
|
+
SÍ → signal() en setup() — REGLA #8
|
|
338
|
+
|
|
339
|
+
¿El dato se deriva de otro signal? (transformación pura)
|
|
340
|
+
SÍ → computed() — readonly, sin side effects
|
|
341
|
+
|
|
342
|
+
¿El dato es controlado por el padre?
|
|
343
|
+
SÍ → prop con type — REGLA #1
|
|
344
|
+
|
|
345
|
+
¿El padre necesita saber cuándo cambia?
|
|
346
|
+
SÍ → emit (kebab-case) — REGLA #2
|
|
347
|
+
|
|
348
|
+
¿Otros componentes sin relación padre↔hijo lo necesitan?
|
|
349
|
+
SÍ → defineStore() — REGLA #9 (no provide/inject)
|
|
350
|
+
|
|
351
|
+
¿Necesita disparar side effect cuando cambia un signal?
|
|
352
|
+
SÍ → watch(signal, callback)
|
|
353
|
+
|
|
354
|
+
¿Necesita side effect con auto-suscripción a múltiples signals?
|
|
355
|
+
SÍ → effect(() => ...)
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### ¿Te piden COMUNICAR componentes?
|
|
359
|
+
|
|
360
|
+
```
|
|
361
|
+
¿Padre → Hijo?
|
|
362
|
+
→ prop con type (datos)
|
|
363
|
+
→ callback como prop (acciones que el hijo invoca)
|
|
364
|
+
|
|
365
|
+
¿Hijo → Padre?
|
|
366
|
+
Opción A (canónica): emit + @event en el padre
|
|
367
|
+
Hijo: emits: ['save'], emit('save', payload)
|
|
368
|
+
Padre: <Child @save="actions.onSave" />
|
|
369
|
+
El handler del padre recibe el payload directo (sin Event)
|
|
370
|
+
|
|
371
|
+
Opción B (callback como prop): cuando el hijo no "owns" el evento
|
|
372
|
+
Padre: <Child :onSave="actions.handle" />
|
|
373
|
+
Hijo: props.onSave declarado como Function
|
|
374
|
+
|
|
375
|
+
¿Hermanos (sin relación)?
|
|
376
|
+
→ defineStore() — signals globales compartidos
|
|
377
|
+
|
|
378
|
+
¿Más de 2 niveles de props drilling?
|
|
379
|
+
→ defineStore()
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
### ¿Te piden RENDERIZAR una lista?
|
|
383
|
+
|
|
384
|
+
```
|
|
385
|
+
1. La colección DEBE ser state.x.value / computed.x.value / props.x — REGLA #5
|
|
386
|
+
2. Sintaxis: v-for="item in <colección>" o v-for="(item, idx) in <colección>"
|
|
387
|
+
3. Si el handler necesita el item: curried action
|
|
388
|
+
function toggle(id: string) { return () => { ... } }
|
|
389
|
+
@click="actions.toggle(item.id)"
|
|
390
|
+
4. Si :class depende de un signal Y del item: computed que retorna closure con SNAPSHOT
|
|
391
|
+
const itemClass = computed(() => {
|
|
392
|
+
const active = state.activeId.value // ← lee la dep ACÁ
|
|
393
|
+
return (item) => item.id === active ? 'A' : 'B'
|
|
394
|
+
})
|
|
395
|
+
:class="computed.itemClass.value(item)"
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### ¿Te piden HACER UN FETCH?
|
|
399
|
+
|
|
400
|
+
```
|
|
401
|
+
NUNCA en el componente — REGLA #10
|
|
402
|
+
|
|
403
|
+
PASO 1: crear/modificar service en src/services/<dominio>.service.ts
|
|
404
|
+
export const UserService = createService(
|
|
405
|
+
{ baseUrl: '/api/users' },
|
|
406
|
+
{
|
|
407
|
+
async getAll() { return this.get<User[]>('/') },
|
|
408
|
+
async getById(id: string) { return this.get<User>(`/${id}`) },
|
|
409
|
+
},
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
PASO 2: en el componente, import y llamar desde action u onMount
|
|
413
|
+
import { UserService } from '../services/user.service'
|
|
414
|
+
|
|
415
|
+
const users = signal<User[]>([])
|
|
416
|
+
onMount(async () => {
|
|
417
|
+
users.value = await UserService.getAll()
|
|
418
|
+
})
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
### ¿Te piden MODIFICAR un componente existente?
|
|
422
|
+
|
|
423
|
+
```
|
|
424
|
+
PASO 1: Leer el .ark completo (no asumir su shape)
|
|
425
|
+
PASO 2: Identificar dónde va el cambio según el ORDEN del setup() (§3 REGLA #16):
|
|
426
|
+
- Nuevo signal → bloque 1
|
|
427
|
+
- Nuevo computed → bloque 2
|
|
428
|
+
- Nuevo onMount → bloque 3
|
|
429
|
+
- Nuevo watch → bloque 4
|
|
430
|
+
- Nuevo action → bloque 5
|
|
431
|
+
- Nueva prop → en props: {} con type
|
|
432
|
+
- Nuevo emit → en emits: [] kebab-case
|
|
433
|
+
- Nuevo slot → en el template (cuidado, suma una API pública nueva)
|
|
434
|
+
PASO 3: Si agregás un signal/computed/action, registrarlo en el return
|
|
435
|
+
PASO 4: Correr ark analyze para confirmar 0 errors nuevos
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### ¿El analyzer reportó un error?
|
|
439
|
+
|
|
440
|
+
```
|
|
441
|
+
PASO 1: Leer el message Y el campo `Fix:` (siempre viene con sugerencia concreta)
|
|
442
|
+
PASO 2: Si el fix dice "Reemplazar X por Y": aplicar literal
|
|
443
|
+
PASO 3: Si el fix dice "Crear un X y referenciarlo": crear primero, luego referenciar
|
|
444
|
+
PASO 4: Re-correr ark analyze para confirmar que el error desapareció
|
|
445
|
+
PASO 5: Si aparece OTRO error: repetir (a veces un fix expone uno latente)
|
|
446
|
+
|
|
447
|
+
NO ignorar errors. NO desactivar reglas. NO ignorar warnings sin entender por qué aparecen.
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
---
|
|
451
|
+
|
|
452
|
+
## 5. PLANTILLA CANÓNICA — componente completo
|
|
453
|
+
|
|
454
|
+
Copiar y adaptar. Toda IA debe partir de esta plantilla para crear un componente nuevo.
|
|
455
|
+
|
|
456
|
+
```html
|
|
457
|
+
<template>
|
|
458
|
+
<div class="user-card">
|
|
459
|
+
<!-- Interpolación -->
|
|
460
|
+
<h2>{{ props.name }}</h2>
|
|
461
|
+
<p>{{ state.bio.value }}</p>
|
|
462
|
+
<span>{{ computed.followLabel.value }}</span>
|
|
463
|
+
|
|
464
|
+
<!-- Evento — SIEMPRE actions.xxx (REGLA #4) -->
|
|
465
|
+
<button @click="actions.follow">{{ computed.followLabel.value }}</button>
|
|
466
|
+
|
|
467
|
+
<!-- Input controlado — REGLA #7 -->
|
|
468
|
+
<input :value="state.bio.value" @input="actions.onBioInput" />
|
|
469
|
+
|
|
470
|
+
<!-- Condicional -->
|
|
471
|
+
<div v-if="state.following.value">Siguiendo</div>
|
|
472
|
+
<div v-else>No seguido</div>
|
|
473
|
+
|
|
474
|
+
<!-- Lista — colección namespaced (REGLA #5) -->
|
|
475
|
+
<div v-for="tag in state.tags.value">{{ tag }}</div>
|
|
476
|
+
|
|
477
|
+
<!-- v-show — mantiene en DOM -->
|
|
478
|
+
<div v-show="state.panelVisible.value">Panel</div>
|
|
479
|
+
</div>
|
|
480
|
+
</template>
|
|
481
|
+
|
|
482
|
+
<script lang="ts">
|
|
483
|
+
import { defineComponent, signal, computed, watch, onMount } from 'arckode-ui'
|
|
484
|
+
|
|
485
|
+
export default defineComponent({
|
|
486
|
+
name: 'UserCard', // REGLA: PascalCase obligatorio
|
|
487
|
+
|
|
488
|
+
props: { // REGLA #1
|
|
489
|
+
name: { type: String, required: true },
|
|
490
|
+
userId: { type: String, required: true },
|
|
491
|
+
},
|
|
492
|
+
|
|
493
|
+
emits: ['follow', 'unfollow'], // REGLA #2: kebab-case
|
|
494
|
+
|
|
495
|
+
setup(props, { emit }) {
|
|
496
|
+
// ── 1. signals ──
|
|
497
|
+
const bio = signal('')
|
|
498
|
+
const following = signal(false)
|
|
499
|
+
const tags = signal<string[]>([])
|
|
500
|
+
const panelVisible = signal(true)
|
|
501
|
+
|
|
502
|
+
// ── 2. computed ──
|
|
503
|
+
const followLabel = computed(() =>
|
|
504
|
+
following.value ? 'Dejar de seguir' : 'Seguir'
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
// ── 3. lifecycle ──
|
|
508
|
+
onMount(() => {
|
|
509
|
+
bio.value = 'Cargando...'
|
|
510
|
+
return () => { /* cleanup opcional */ }
|
|
511
|
+
})
|
|
512
|
+
|
|
513
|
+
// ── 4. watch ──
|
|
514
|
+
watch(following, (newVal) => { console.log('following:', newVal) })
|
|
515
|
+
|
|
516
|
+
// ── 5. actions — REGLA #12: function declaration ──
|
|
517
|
+
function follow() {
|
|
518
|
+
following.value = !following.value
|
|
519
|
+
emit(following.value ? 'follow' : 'unfollow', { userId: props.userId })
|
|
520
|
+
}
|
|
521
|
+
function onBioInput(e: Event) {
|
|
522
|
+
bio.value = (e.target as HTMLInputElement).value
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// ── 6. return — REGLA #3: SOLO state/computed/actions ──
|
|
526
|
+
return {
|
|
527
|
+
state: { bio, following, tags, panelVisible },
|
|
528
|
+
computed: { followLabel },
|
|
529
|
+
actions: { follow, onBioInput },
|
|
530
|
+
}
|
|
531
|
+
},
|
|
532
|
+
})
|
|
533
|
+
</script>
|
|
534
|
+
|
|
535
|
+
<style scoped>
|
|
536
|
+
.user-card { padding: 1rem; }
|
|
537
|
+
</style>
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
---
|
|
541
|
+
|
|
542
|
+
## 6. PLANTILLAS POR TIPO
|
|
543
|
+
|
|
544
|
+
### 6.1 — Componente atómico (Badge, Button)
|
|
545
|
+
|
|
546
|
+
```html
|
|
547
|
+
<template>
|
|
548
|
+
<span :class="computed.classes.value"><slot /></span>
|
|
549
|
+
</template>
|
|
550
|
+
<script lang="ts">
|
|
551
|
+
import { defineComponent, computed } from 'arckode-ui'
|
|
552
|
+
const VARIANTS: Record<string, string> = { low: '...', high: '...' }
|
|
553
|
+
export default defineComponent({
|
|
554
|
+
name: 'Badge',
|
|
555
|
+
props: { variant: { type: String, required: true } },
|
|
556
|
+
emits: [],
|
|
557
|
+
setup(props) {
|
|
558
|
+
const classes = computed(() => VARIANTS[props.variant as string] ?? VARIANTS['low'])
|
|
559
|
+
return { state: {}, computed: { classes }, actions: {} }
|
|
560
|
+
},
|
|
561
|
+
})
|
|
562
|
+
</script>
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
### 6.2 — Wrapper con slot (Modal, Card)
|
|
566
|
+
|
|
567
|
+
```html
|
|
568
|
+
<template>
|
|
569
|
+
<div v-show="props.visible" class="modal">
|
|
570
|
+
<div class="header">
|
|
571
|
+
<h2>{{ props.title }}</h2>
|
|
572
|
+
<button @click="actions.close">✕</button>
|
|
573
|
+
</div>
|
|
574
|
+
<div class="body"><slot /></div>
|
|
575
|
+
</div>
|
|
576
|
+
</template>
|
|
577
|
+
<script lang="ts">
|
|
578
|
+
import { defineComponent } from 'arckode-ui'
|
|
579
|
+
export default defineComponent({
|
|
580
|
+
name: 'Modal',
|
|
581
|
+
props: {
|
|
582
|
+
visible: { type: Boolean, required: true },
|
|
583
|
+
title: { type: String, required: true },
|
|
584
|
+
onClose: { type: Function, required: true },
|
|
585
|
+
},
|
|
586
|
+
emits: [],
|
|
587
|
+
setup(props) {
|
|
588
|
+
function close() { (props.onClose as () => void)() }
|
|
589
|
+
return { state: {}, computed: {}, actions: { close } }
|
|
590
|
+
},
|
|
591
|
+
})
|
|
592
|
+
</script>
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
### 6.3 — Lista con curried action
|
|
596
|
+
|
|
597
|
+
```html
|
|
598
|
+
<template>
|
|
599
|
+
<div v-for="item in state.items.value">
|
|
600
|
+
<button @click="actions.toggle(item.id)">{{ item.done ? '✓' : '○' }}</button>
|
|
601
|
+
<span>{{ item.text }}</span>
|
|
602
|
+
</div>
|
|
603
|
+
</template>
|
|
604
|
+
<script lang="ts">
|
|
605
|
+
import { defineComponent, signal } from 'arckode-ui'
|
|
606
|
+
interface Item { id: string; text: string; done: boolean }
|
|
607
|
+
export default defineComponent({
|
|
608
|
+
name: 'ItemList',
|
|
609
|
+
props: {},
|
|
610
|
+
emits: [],
|
|
611
|
+
setup() {
|
|
612
|
+
const items = signal<Item[]>([])
|
|
613
|
+
function toggle(id: string) {
|
|
614
|
+
return () => {
|
|
615
|
+
items.value = items.value.map(i => i.id === id ? { ...i, done: !i.done } : i)
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return { state: { items }, computed: {}, actions: { toggle } }
|
|
619
|
+
},
|
|
620
|
+
})
|
|
621
|
+
</script>
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
---
|
|
625
|
+
|
|
626
|
+
## 7. STORE — estado global compartido
|
|
627
|
+
|
|
628
|
+
```typescript
|
|
629
|
+
// stores/user.store.ts
|
|
630
|
+
import { defineStore, signal, computed } from 'arckode-ui'
|
|
631
|
+
|
|
632
|
+
interface UserDTO { id: string; name: string }
|
|
633
|
+
|
|
634
|
+
export const useUserStore = defineStore('user', {
|
|
635
|
+
state: {
|
|
636
|
+
current: signal<UserDTO | null>(null),
|
|
637
|
+
loading: signal(false),
|
|
638
|
+
},
|
|
639
|
+
getters: {
|
|
640
|
+
isLoggedIn: computed(() => useUserStore().state.current.value !== null),
|
|
641
|
+
},
|
|
642
|
+
actions: {
|
|
643
|
+
async login(email: string, password: string) {
|
|
644
|
+
const store = useUserStore()
|
|
645
|
+
store.state.loading.value = true
|
|
646
|
+
// ...
|
|
647
|
+
},
|
|
648
|
+
logout() { useUserStore().state.current.value = null },
|
|
649
|
+
},
|
|
650
|
+
})
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
Uso en componente:
|
|
654
|
+
```typescript
|
|
655
|
+
const userStore = useUserStore()
|
|
656
|
+
return {
|
|
657
|
+
state: { current: userStore.state.current },
|
|
658
|
+
computed: { isLoggedIn: userStore.getters.isLoggedIn },
|
|
659
|
+
actions: { logout: userStore.actions.logout },
|
|
660
|
+
}
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
**Patrón clave:** dentro de getters/actions, llamar `useUserStore()` para acceder al state. Es feo pero es la API real verificada en tests.
|
|
664
|
+
|
|
665
|
+
---
|
|
666
|
+
|
|
667
|
+
## 8. SERVICE — HTTP
|
|
668
|
+
|
|
669
|
+
```typescript
|
|
670
|
+
// services/product.service.ts
|
|
671
|
+
import { createService } from 'arckode-ui'
|
|
672
|
+
|
|
673
|
+
interface Product { id: string; name: string; price: number }
|
|
674
|
+
|
|
675
|
+
export const ProductService = createService(
|
|
676
|
+
{ baseUrl: '/api/products', timeout: 5000 },
|
|
677
|
+
{
|
|
678
|
+
async getAll() { return this.get<Product[]>('/') },
|
|
679
|
+
async getById(id: string) { return this.get<Product>(`/${id}`) },
|
|
680
|
+
async create(data: Omit<Product, 'id'>) { return this.post<Product>('/', data) },
|
|
681
|
+
async update(id: string, data: Partial<Product>) { return this.put<Product>(`/${id}`, data) },
|
|
682
|
+
async remove(id: string) { return this.delete<void>(`/${id}`) },
|
|
683
|
+
},
|
|
684
|
+
)
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
**Firma del createService: DOS ARGUMENTOS (options, definition).** Verificado en tests.
|
|
688
|
+
|
|
689
|
+
---
|
|
690
|
+
|
|
691
|
+
## 9. CLI — comandos exactos
|
|
692
|
+
|
|
693
|
+
```bash
|
|
694
|
+
ark new <nombre> # scaffold proyecto nuevo
|
|
695
|
+
ark generate component <PascalCase> # alias: ark g c
|
|
696
|
+
ark generate page <name> # alias: ark g p — name puede ser kebab o users/[id]
|
|
697
|
+
ark generate store <camelCase> # alias: ark g s
|
|
698
|
+
ark generate service <PascalCase> # alias: ark g sv
|
|
699
|
+
ark generate layout <name> # alias: ark g l
|
|
700
|
+
ark analyze [--json] # detecta violations en .ark
|
|
701
|
+
ark routes # lista rutas detectadas en src/pages/
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
Naming OBLIGATORIO (el generator lo enforza):
|
|
705
|
+
- Component: PascalCase (Button, UserCard)
|
|
706
|
+
- Store: camelCase (cart, userPreferences)
|
|
707
|
+
- Service: PascalCase (Product, User)
|
|
708
|
+
- Page/Layout: kebab-case con segmentos
|
|
709
|
+
|
|
710
|
+
---
|
|
711
|
+
|
|
712
|
+
## 10. VITE — setup del proyecto consumidor
|
|
713
|
+
|
|
714
|
+
```typescript
|
|
715
|
+
// vite.config.ts
|
|
716
|
+
import { defineConfig } from 'vite'
|
|
717
|
+
import { arkcodeUi } from 'arckode-ui/vite' // REGLA #15: nombre EXACTO
|
|
718
|
+
|
|
719
|
+
export default defineConfig({
|
|
720
|
+
plugins: [
|
|
721
|
+
arkcodeUi({
|
|
722
|
+
analyzer: {
|
|
723
|
+
failOnWarnings: false, // dev: no bloquea. prod: bloquea errors.
|
|
724
|
+
ignore: [], // códigos a ignorar
|
|
725
|
+
},
|
|
726
|
+
}),
|
|
727
|
+
],
|
|
728
|
+
})
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
```html
|
|
732
|
+
<!-- index.html -->
|
|
733
|
+
<div id="app"></div>
|
|
734
|
+
<script type="module" src="/src/main.ts"></script>
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
```typescript
|
|
738
|
+
// src/main.ts
|
|
739
|
+
import { mount } from 'arckode-ui'
|
|
740
|
+
import App from './App.ark'
|
|
741
|
+
|
|
742
|
+
mount(App, '#app') // REGLA #14: selector string
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
---
|
|
746
|
+
|
|
747
|
+
## 11. ANALYZER OUTPUT — cómo leerlo
|
|
748
|
+
|
|
749
|
+
Cada violation viene con `Fix:` cuando es determinístico. **Leer el Fix y aplicarlo literal.**
|
|
750
|
+
|
|
751
|
+
```
|
|
752
|
+
[arckode-ui] UserCard.ark:5
|
|
753
|
+
HANDLER_NOT_IN_ACTIONS: El handler "handleClick" no está namespaced.
|
|
754
|
+
|
|
755
|
+
> 5 | <button @click="handleClick">
|
|
756
|
+
Fix: Reemplazar "handleClick" por "actions.handleClick" y asegurarse de que la función esté declarada en setup() y exportada en el return.actions.
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
Códigos del analyzer (16):
|
|
760
|
+
|
|
761
|
+
| Código | Severity | Regla violada |
|
|
762
|
+
|--------|----------|---------------|
|
|
763
|
+
| MISSING_TEMPLATE | error | — (estructura) |
|
|
764
|
+
| MISSING_SCRIPT | error | — |
|
|
765
|
+
| MISSING_LANG_TS | error | — |
|
|
766
|
+
| WRONG_TEMPLATE_ORDER | error | — |
|
|
767
|
+
| MISSING_COMPONENT_NAME | error | — |
|
|
768
|
+
| PROP_MISSING_TYPE | error | REGLA #1 |
|
|
769
|
+
| EMIT_CAMELCASE | error | REGLA #2 |
|
|
770
|
+
| SETUP_UNKNOWN_RETURN_KEY | error | REGLA #3 |
|
|
771
|
+
| HANDLER_NOT_IN_ACTIONS | error | REGLA #4 |
|
|
772
|
+
| VFOR_NOT_NAMESPACED | error | REGLA #5 |
|
|
773
|
+
| VIF_NOT_NAMESPACED | warning | REGLA #6 |
|
|
774
|
+
| REF_REACTIVE_USAGE | error | REGLA #8 |
|
|
775
|
+
| PROVIDE_INJECT_USAGE | error | REGLA #9 |
|
|
776
|
+
| DIRECT_FETCH_IN_COMPONENT | error | REGLA #10 |
|
|
777
|
+
| LOGIC_IN_TEMPLATE | error | REGLA #11 |
|
|
778
|
+
| ARROW_FUNCTION_ACTION | warning | REGLA #12 |
|
|
779
|
+
|
|
780
|
+
---
|
|
781
|
+
|
|
782
|
+
## 12. CHECKLIST POST-CÓDIGO (antes de declarar "listo")
|
|
783
|
+
|
|
784
|
+
```
|
|
785
|
+
□ npx ark analyze → 0 errors
|
|
786
|
+
□ Si tocaste el framework: bun test → todos verdes
|
|
787
|
+
□ Si tocaste UI: arrancar dev server y probar en navegador
|
|
788
|
+
□ Si agregaste prop/emit nuevo: actualizar consumidores
|
|
789
|
+
□ Si modificaste un componente que tenía tests: re-correr esos tests
|
|
790
|
+
□ ¿Toqué exports del barrel src/index.ts del framework? → es breaking change
|
|
791
|
+
□ ¿Cambié el código o severity de una violation? → es breaking change
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
**Si el analyzer reporta errors → corregirlos según el campo Fix.**
|
|
795
|
+
**Si los tests fallan → corregir antes de continuar.**
|
|
796
|
+
**No declarar "listo" hasta que los 2 estén verdes.**
|
|
797
|
+
|
|
798
|
+
---
|
|
799
|
+
|
|
800
|
+
## 13. ANTI-PATTERNS — referencia rápida (todo viola alguna regla numerada)
|
|
801
|
+
|
|
802
|
+
```typescript
|
|
803
|
+
// ❌ REGLA #1
|
|
804
|
+
props: { name: { required: true } }
|
|
805
|
+
props: { name: { type: 'string' } }
|
|
806
|
+
|
|
807
|
+
// ❌ REGLA #2
|
|
808
|
+
emits: ['userSaved']
|
|
809
|
+
|
|
810
|
+
// ❌ REGLA #3
|
|
811
|
+
return { state: {}, computed: {}, actions: {}, methods: {} }
|
|
812
|
+
|
|
813
|
+
// ❌ REGLA #4
|
|
814
|
+
@click="handleClick"
|
|
815
|
+
@click="state.count.value++"
|
|
816
|
+
|
|
817
|
+
// ❌ REGLA #5
|
|
818
|
+
v-for="item in items"
|
|
819
|
+
v-for="x in getList()"
|
|
820
|
+
|
|
821
|
+
// ❌ REGLA #6 (warning)
|
|
822
|
+
v-if="visible"
|
|
823
|
+
|
|
824
|
+
// ❌ REGLA #7
|
|
825
|
+
<input v-model="state.name" />
|
|
826
|
+
|
|
827
|
+
// ❌ REGLA #8
|
|
828
|
+
const x = ref(0)
|
|
829
|
+
const s = reactive({})
|
|
830
|
+
|
|
831
|
+
// ❌ REGLA #9
|
|
832
|
+
provide('key', val)
|
|
833
|
+
inject('key')
|
|
834
|
+
|
|
835
|
+
// ❌ REGLA #10
|
|
836
|
+
onMount(() => fetch('/api/x'))
|
|
837
|
+
|
|
838
|
+
// ❌ REGLA #11
|
|
839
|
+
@click="state.count.value++"
|
|
840
|
+
:class="state.x.value ? 'a' : 'b'"
|
|
841
|
+
|
|
842
|
+
// ❌ REGLA #12
|
|
843
|
+
actions: { submit: () => {} }
|
|
844
|
+
|
|
845
|
+
// ❌ REGLA #13
|
|
846
|
+
<usercard :prop="x" />
|
|
847
|
+
|
|
848
|
+
// ❌ REGLA #14
|
|
849
|
+
mount(App, document.getElementById('app')!)
|
|
850
|
+
|
|
851
|
+
// ❌ REGLA #15
|
|
852
|
+
import { ref } from 'vue'
|
|
853
|
+
import { arckodePlugin } from 'arckode-ui/vite' // nombre incorrecto
|
|
854
|
+
```
|
|
855
|
+
|
|
856
|
+
---
|
|
857
|
+
|
|
858
|
+
## 14. ESTADO ACTUAL DEL FRAMEWORK (v0.1.1)
|
|
859
|
+
|
|
860
|
+
- ✅ 314 tests passing
|
|
861
|
+
- ✅ 16 reglas del analyzer con `Fix:` field
|
|
862
|
+
- ✅ APIs exportadas: signal, computed, watch, effect, defineComponent, onMount, onUnmount, onUpdate, h, mount, mountComponent, defineStore, createService, ArkServiceError, createRouter, navigate, useRoute, getCurrentPath
|
|
863
|
+
- ✅ Subpath `arckode-ui/vite` exporta `arkcodeUi`
|
|
864
|
+
- ✅ CLI `ark` con shebang ejecutable
|
|
865
|
+
- ✅ 6 sub-skills distribuidos en `skills/`
|
|
866
|
+
- ✅ 2 ejemplos completos: `kitchen-sink` (demo de features), `tasks` (app real con sidebar/modal/form)
|