arckode-ui
v0.29.4
Published
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.
Maintainers
Readme
Arckode UI
Framework frontend con archivos .ark (Single File Components), reactividad por signals, compiler propio (Vite plugin), router file-system y analyzer estático con sugerencias concretas de fix.
Diseñado para máxima predictibilidad de output de IA. Hay un solo camino correcto para cada problema. Para cada violation detectada, el analyzer sugiere el fix concreto.
Versión: 0.29.1 — scaffold production-grade con auth, guards, layouts
Activar el SKILL para tu IA
ark new frontend genera AGENTS.md y CLAUDE.md automáticamente en la raíz del proyecto.
Para proyectos ya existentes, copiá CLAUDE.md.example:
cp node_modules/arckode-ui/CLAUDE.md.example ./CLAUDE.mdSin este archivo, la IA improvisa. Con el SKILL cargado, tus componentes pasan ark analyze con 0 errores.
Instalación
Opción A — Scaffold completo (recomendado)
# Crea un proyecto FUNCIONAL con todas las deps configuradas
npx arckode-ui new mi-app
cd mi-app
bun install # o npm install / pnpm install
bun dev # arranca http://localhost:5173Qué genera ark new automáticamente (no tenés que escribir nada de esto a mano):
mi-app/
├─ package.json ← Vite 5.4 pineado, Tailwind v4, TypeScript
├─ vite.config.ts ← Plugins arkcodeUi() + tailwindcss() ya configurados
├─ tsconfig.json ← Strict mode + paths correctos
├─ AGENTS.md / CLAUDE.md ← Instrucciones para IA (puntero a node_modules/arckode-ui/skills)
├─ index.html ← Con <div id="app"> + script type=module
├─ src/
│ ├─ main.ts ← Entry: lazy routes + mount(App)
│ ├─ App.ark ← Componente raíz con auth check on mount
│ ├─ guards.ts ← protectPage() y requireAuth()
│ ├─ styles/app.css ← Tailwind v4 con @theme (tokens: primary, surface, text, etc.)
│ ├─ layouts/MainLayout.ark ← Navbar auth-aware + slot + ToastContainer
│ ├─ pages/ ← index.ark, login.ark, register.ark (Skeleton + EmptyState + SEO)
│ ├─ components/ui/ ← 52 primitivos canónicos (Button, Input, Card, Dialog, etc.)
│ ├─ stores/ ← 4 stores: toast, confirm, auth (con isAuthenticated), i18n
│ ├─ composables/ ← 3 composables: useFetch, useConfirm, useRequireAuth
│ ├─ services/auth.service.ts ← createService con login/register/me/logout
│ ├─ types/index.ts ← DTOs: User, ApiEnvelope, PaginatedResponse
│ └─ shared/ ← icons.ts (Lucide subset), validation.ts (createSchema)No improvises con primitivos custom — los 52 que vienen scaffoldeados cubren ~98% de UI típica.
Opción B — Instalación manual (proyecto existente)
# Producción
bun add arckode-ui
# Desarrollo (versiones EXACTAS — no cambiar)
bun add -d [email protected] @tailwindcss/vite@4 tailwindcss@4 typescript@5
# o con npm
npm install arckode-ui
npm install -D [email protected] @tailwindcss/vite@4 tailwindcss@4 typescript@5⚠️ Vite 5.x OBLIGATORIO — el plugin usa
transformWithEsbuild, que Vite 6 removió. Con Vite 6+ el build falla conFailed to load transformWithEsbuild. Si tupackage.jsontiene"vite": "^6.0.0"o"vite": "latest", bajalo a"vite": "^5.4.0".
⚠️ Tailwind v4 OBLIGATORIO — la sintaxis cambió. v4 usa
@import "tailwindcss"(NO las directivas v3@tailwind base/components/utilities) y el plugin oficial@tailwindcss/vite(NO el PostCSS plugin). Si copiás un setup de Tailwind v3, no aplica ningún estilo.
Configuración mínima manual
// vite.config.ts
import { defineConfig } from 'vite'
import { arkcodeUi } from 'arckode-ui/vite' // ← Plugin obligatorio
import tailwindcss from '@tailwindcss/vite' // ← Plugin oficial Tailwind v4
export default defineConfig({
plugins: [arkcodeUi(), tailwindcss()],
})/* src/styles/global.css — Tailwind v4 con @theme */
@import "tailwindcss";
@theme {
--color-primary: oklch(0.65 0.18 250);
--color-surface: oklch(0.16 0 0);
--color-text: oklch(0.95 0 0);
--color-text-muted: oklch(0.65 0 0);
--color-border: oklch(0.25 0 0);
/* Cualquier --color-X genera utilities bg-X, text-X, border-X automáticamente */
}
@themees CSS-first (Tailwind v4). NO haytailwind.config.jsnitailwind.config.ts. Para customizar la línea gráfica, editás directamente este archivo.ark theme initgenera un baseline completo con tokens semánticos.
// src/main.ts
import './styles/global.css'
import { mount } from 'arckode-ui'
import App from './App.ark'
mount(App, '#app')<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Mi App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>Por qué el plugin de Vite es obligatorio (cómo funciona el stack)
.ark source (template + script + style)
│
▼
arkcodeUi() plugin (Vite plugin)
│
├──▶ parser.ts → divide en {template, script, style}
├──▶ template-compiler→ <template> a render function (h() calls)
├──▶ analyzer.ts → valida 40+ reglas, bloquea build si hay errores
├──▶ scope-css.ts → si <style scoped>: prefija con [data-ark-XXX]
└──▶ transformWithEsbuild → strip TS types, output JS final
│
▼
JS module compilado + CSS scoped inyectado en <head>
│
▼
Vite bundle (dev: HMR / prod: Rollup)Sin el plugin, los .ark son archivos opacos que Vite no entiende — el build falla con Unknown file extension ".ark". El plugin ES el framework: no hay forma de usar arckode-ui sin él.
Troubleshooting — errores comunes
| Error | Causa | Solución |
|---|---|---|
| Failed to load transformWithEsbuild | Tenés Vite 6+ instalado | Bajar a Vite 5.4: bun add -d [email protected] |
| Unknown file extension ".ark" | El plugin arkcodeUi() no está en vite.config.ts | Agregar plugins: [arkcodeUi(), ...] |
| arkcodeUi is not a function | Import mal (arckodeUi vs arkcodeUi) | Correcto: import { arkcodeUi } from 'arckode-ui/vite' (K minúscula intermedia) |
| Tailwind no aplica estilos | Sintaxis v3 en proyecto v4, o @tailwindcss/vite falta | @import "tailwindcss" (NO @tailwind base/...) + tailwindcss() en plugins |
| Identifier 'defineComponent' has already been declared | Mito histórico de versiones viejas | Ya no pasa — esbuild deduplica. Importalo normalmente. |
| [Component] X es requerido en runtime | Usaste un primitivo (<Button>) sin pasar todos los props obligatorios | El primitivo te dice qué falta (name: 'Button' + prop faltante) |
| Build pasa pero pantalla en blanco | mount(App, '#app') con #app que no existe en index.html | Verificar el ID del <div> en index.html |
| process is not defined en runtime | Importaste algo de arckode-ui/server en código que va al cliente | El subpath /server es solo para Node. Usar arckode-ui (sin /server) en .ark |
SSG — HTML estático por ruta (v0.20+)
Cada ruta tiene su propio HTML pre-rendered. Bots (Google, WhatsApp, Twitter, Discord) leen el HTML estático con <title>, <meta>, <link rel="canonical"> correctos — sin ejecutar JS.
Setup
1) Cada page del router define su head con useHead():
// src/pages/about.ark
<script lang="ts">
import { defineComponent, useHead } from 'arckode-ui'
export default defineComponent({
name: 'AboutPage',
setup() {
useHead({
title: 'Sobre nosotros · Mi App',
description: 'Quiénes somos y qué hacemos.',
meta: [
{ property: 'og:title', content: 'Sobre nosotros · Mi App' },
{ property: 'og:image', content: 'https://mi-app.com/og-about.png' },
{ name: 'twitter:card', content: 'summary_large_image' },
],
link: [{ rel: 'canonical', href: 'https://mi-app.com/about' }],
})
return { state: {}, computed: {}, actions: {} }
},
})
</script>2) Para rutas dinámicas (src/pages/users/[id].ark), exportá staticPaths:
// src/pages/users/[id].ark
export const staticPaths = [
{ id: '1' },
{ id: '2' },
// O cargado de un servicio:
// ...await userService.listIds().then(ids => ids.map(id => ({ id })))
]3) Comandos de build (3 pasos):
bun vite build # bundle SPA normal + dist/index.html
bun vite build --ssr 'src/pages/**/*.ark' --outDir dist/_ssg # SSR build de las pages
npx ark ssg # genera HTML por rutaOutput:
dist/
├─ index.html ← HomePage con su useHead
├─ about/index.html ← AboutPage con su useHead
├─ users/
│ ├─ 1/index.html ← users/[id] con id=1
│ └─ 2/index.html ← users/[id] con id=2
└─ _ssg/ ← Temporal, agregar a .gitignoreServís dist/ con cualquier CDN/server estático (Vercel, Netlify, Cloudflare Pages, Nginx, etc.).
API server programática
Si querés controlarlo desde un script propio (CI custom, integración con otro stack):
import { renderToString, injectHeadAndBody } from 'arckode-ui/server'
import AboutPage from './pages/about.ark' // compilado por vite SSR build
const { html, head } = renderToString(AboutPage, { /* props opcionales */ })
const template = fs.readFileSync('dist/index.html', 'utf-8')
const finalHtml = injectHeadAndBody(template, head, html)
fs.writeFileSync('dist/about/index.html', finalHtml)Limitaciones del SSG actual
- Hidratación: el cliente re-renderiza sobre el HTML pre-rendered (flicker mínimo). Hidratación pulida sin flicker está en backlog (v0.21).
- Server components / per-request SSR: no — todo el render es en build-time. Para sitios cuyo contenido cambia minuto a minuto, considerar ISR (v0.22).
- Async setup:
setup()no es async hoy. Para cargar data antes del SSG, usástaticPathscon loaders o llamáawaiten tu script de build.
Tu primer componente — App.ark
<template>
<div>
<h1>{{ state.message }}</h1>
<button @click="actions.toggle">{{ computed.buttonLabel }}</button>
</div>
</template>
<script lang="ts">
import { defineComponent, signal, computed } from 'arckode-ui'
export default defineComponent({
name: 'App',
props: {},
emits: [],
setup(props, { emit }) {
const message = signal('Hola Arckode')
const buttonLabel = computed(() => message.value === 'Hola Arckode' ? 'Chau' : 'Hola')
function toggle() {
message.value = message.value === 'Hola Arckode' ? 'Chau Arckode' : 'Hola Arckode'
}
return {
state: { message },
computed: { buttonLabel },
actions: { toggle },
}
},
})
</script>Importá
defineComponentnormalmente para que TypeScript y tu IDE lo reconozcan. El Vite plugin ya inyecta un import al compilar — esbuild deduplica los duplicados, así que no rompe. Los 52 primitivos del scaffold (ark new) lo importan así.
En el
<template>accedés a signals SIN.value(Proxy auto-resuelve). En el<script>SÍ usás.valuepara leer/mutar.
CLI
ark new mi-app # scaffold proyecto nuevo
ark generate component UserCard # → src/components/features/UserCard.ark
ark generate page about # → src/pages/about.ark
ark generate page users/[id] # → src/pages/users/[id].ark
ark generate store cart # → src/stores/cart.store.ts
ark generate service Product # → src/services/product.service.ts
ark generate layout users # → src/pages/users/_layout.ark
ark analyze [--json] # detecta violations en archivos .ark
ark routes # lista rutas detectadas en src/pages/Aliases del generator: c (component), p (page), s (store), sv (service), l (layout).
Reactividad — Signals
import { signal, computed, watch, effect } from 'arckode-ui'
const count = signal(0)
count.value = 5 // set — dispara re-render
count.value // get — suscribe al efecto activo
count.peek // get sin suscribirse
const double = computed(() => count.value * 2)
double.value // readonly — recalcula al cambiar count
watch(count, (newVal, oldVal) => { /* ... */ })
effect(() => { document.title = `${count.value}` })Directivas del template
| Necesito | Sintaxis |
|----------|----------|
| Texto dinámico | {{ state.x }} / {{ computed.x }} / {{ props.x }} |
| Atributo dinámico | :value="state.x" / :class="..." |
| Evento | @click="actions.handler" |
| Evento con argumento | @click="actions.toggle(item.id)" |
| Tecla modificadora | @keydown.enter="actions.search" |
| Condicional | v-if="..." + v-else-if="..." + v-else |
| Visibilidad (sin desmontar) | v-show="state.x" |
| Lista | v-for="item in state.items" |
| Lista con identidad (recomendado) | v-for="item in state.items" + :key="item.id" |
| Componente hijo | <MiComponente :prop="state.x" /> |
| Slot (en wrapper) | <slot /> |
| Nodo controlado por librería externa | <div uncontrolled> (Leaflet, Chart.js — el reconciler no toca el contenido) |
.valueno se usa en el template. El renderer envuelvestateycomputeden un Proxy que llama.valueautomáticamente. En el<script>,.valuesigue siendo obligatorio para leer y mutar signals.
:keyenv-foractiva la reconciliación por identidad (preserva input focus, scroll position, estado interno de componentes hijos al reordenar). Sin:keyse cae al patch posicional, que es correcto pero pierde estado al insertar/reordenar.
Error boundaries — onError
Un error en setup() o render de un componente NO tumba el resto de la app. El renderer captura, dispara los handlers declarados con onError, y propaga por la cadena de ancestros hasta encontrar uno que retorne true. Si nadie suprime, se loggea y se renderiza <span data-ark-error>.
import { onError, signal } from 'arckode-ui'
export default defineComponent({
name: 'UserList',
setup() {
const users = signal([])
onError((err) => {
console.error('UserList falló:', err)
toast.actions.error('No pudimos cargar los usuarios. Reintentá en un momento.')
return true // suprimir — no propagar al padre
})
return { state: { users }, computed: {}, actions: {} }
},
})return true→ suprime el error.return undefined/false→ propaga al ancestro.
Para fallback de UI a nivel ruta, definí src/pages/_error.ark. Para errores locales con UI fallback, manejá un signal hasError en el propio setup.
Scoped CSS — <style scoped>
Soportado nativamente desde v0.16. Cada selector se prefija con [data-ark-{scopeId}]. El plugin Vite inyecta el CSS en <head> con id determinístico (HMR-safe).
<template>
<div class="card">
<h2 class="title">{{ props.title }}</h2>
</div>
</template>
<script lang="ts">
export default defineComponent({
name: 'Card',
props: { title: { type: String, required: true } },
setup: () => ({ state: {}, computed: {}, actions: {} }),
})
</script>
<style scoped>
.card { padding: 16px; border-radius: 12px; }
.card .title { font-size: 18px; }
/* :global() escapa el scope */
:global(body.dark) .card { background: #1a1a1a; }
/* @media recursivamente scopea */
@media (max-width: 768px) {
.card { padding: 8px; }
}
/* @keyframes no scopea su contenido */
@keyframes fadeIn { 0% { opacity: 0; } 100% { opacity: 1; } }
</style><style>SINscoped→STYLE_BLOCK_IGNORED(error). CSS verdaderamente global vive UNA vez ensrc/styles/global.css.- Para layouts/responsive típicos preferí Tailwind utilities. Usá
<style scoped>cuando necesités selectores complejos, animaciones o keyframes locales al componente.
Named + scoped slots (v0.17+)
<!-- Card.ark — definición -->
<template>
<div class="card">
<header><slot name="header" /></header>
<main><slot /></main>
<footer><slot name="footer" /></footer>
</div>
</template><!-- App.ark — uso -->
<Card>
<template #header><h1>Mi título</h1></template>
<template #footer><Button>Aceptar</Button></template>
<p>Contenido principal</p>
</Card>Scoped slots (<DataTable> con render custom por fila):
<DataTable :rows="state.users">
<template #default="{ row }">
<Avatar :name="row.name" /> · <Badge>{{ row.role }}</Badge>
</template>
</DataTable>Dynamic components (v0.17+)
<!-- :is recibe el Component o un tag string -->
<component :is="computed.currentTab" :data="state.payload" />Teleport — modals/tooltips fuera del DOM tree (v0.17+)
<Teleport to="#modals">
<div class="dialog">
<h2>Confirmar</h2>
<Button :onClick="actions.confirm">Sí</Button>
</div>
</Teleport>Renderiza los children dentro de #modals (o cualquier selector CSS), dejando un placeholder vacío en su posición original. Escapa overflow:hidden y z-index de ancestros.
Batch — coalesce mutaciones de signals (v0.17+)
import { batch, signal } from 'arckode-ui'
const a = signal(0)
const b = signal(0)
const c = signal(0)
// Sin batch: 3 re-renders
a.value = 1; b.value = 2; c.value = 3
// Con batch: 1 solo re-render
batch(() => {
a.value = 10
b.value = 20
c.value = 30
})Lazy routes — code splitting por ruta
createRouterView acepta () => Promise<Component> como component. Vite emite chunks separados.
import { createRouterView } from 'arckode-ui'
import HomePage from './pages/index.ark'
const routes = [
{ path: '/', component: HomePage }, // eager
{ path: '/heavy', component: () => import('./pages/heavy.ark') }, // lazy
{
path: '/dashboard',
component: () => import('./pages/dashboard.ark'),
loading: () => ({ tag: 'div', props: { class: 'p-8' }, children: ['Cargando…'] }),
error: (err) => ({ tag: 'div', props: { class: 'text-danger' }, children: [String(err)] }),
},
]
mount(createRouterView(routes), '#app')Cache por loader fn — la primera visita carga el chunk, las subsiguientes van directo al componente.
El analyzer — ark analyze
Detecta más de 30 violations comunes. Cada violation incluye un campo fix con la sugerencia concreta:
[arckode-ui] UserCard.ark:5
HANDLER_NOT_IN_ACTIONS: El handler "handleClick" no está namespaced.
> 5 | <button @click="handleClick">
Fix: Reemplazar "handleClick" por "actions.handleClick" y asegurarse de que la función esté declarada en setup() y exportada en el return.actions.Reglas principales:
- Estructura:
MISSING_TEMPLATE,MISSING_SCRIPT,MISSING_LANG_TS,WRONG_TEMPLATE_ORDER,DUPLICATE_SECTION - Componente:
MISSING_COMPONENT_NAME,PROP_MISSING_TYPE,EMIT_CAMELCASE,SETUP_UNKNOWN_RETURN_KEY,SIGNAL_IN_COMPUTED - Reactividad rechazada:
REF_REACTIVE_USAGE,PROVIDE_INJECT_USAGE,V_MODEL_UNSUPPORTED - Red:
DIRECT_FETCH_IN_COMPONENT(solo permitido ensrc/services/) - Template:
LOGIC_IN_TEMPLATE,HANDLER_NOT_IN_ACTIONS,VFOR_NOT_NAMESPACED,VIF_NOT_NAMESPACED,TEMPLATE_UNDEFINED_REF,HTML_COMMENT_IN_TEMPLATE - Style:
STYLE_BLOCK_IGNORED(solo dispara con<style>SINscoped— scoped es soportado),STATIC_INLINE_STYLE,PRIMITIVE_INLINE,OVERRIDE_PRIMITIVE_STYLE,CSS_DUPLICATES_TAILWIND(cross-file) - Arquitectura:
LAYER_IMPORT_VIOLATION,DUPLICATE_INTERFACE(cross-file) - SOLID:
COMPONENT_TOO_LONG,TOO_MANY_PROPS(ISP),TOO_MANY_STORES(SRP),FUNCTION_TOO_LONG(SRP),ARROW_FUNCTION_ACTION - A11y:
IMG_MISSING_ALT,CLICK_ON_NON_INTERACTIVE - UX obligatorio:
BUTTON_ASYNC_NO_LOADING,ASYNC_ACTION_NO_ERROR_HANDLING
Las reglas de lógica (DIRECT_FETCH_IN_COMPONENT, REF_REACTIVE_USAGE, FUNCTION_TOO_LONG, ASYNC_ACTION_NO_ERROR_HANDLING) también corren sobre los .ts de src/composables/. fetch() directo solo se permite en src/services/.
Ejemplos incluidos
examples/kitchen-sink/— demo de todas las directivas y primitivasexamples/tasks/— app real de tareas con sidebar, modal, form, validación, search con debounce
Correr cualquiera:
cd examples/tasks && bun install && bun devDocumentación para IA
CLAUDE.md(en este repo) — instrucciones para Claude/OpenCode al desarrollar el framework~/.claude/skills/arckode-ui/SKILL.md— guía completa para usar el framework
Gotchas de setup
| Síntoma | Causa | Fix |
|---------|-------|-----|
| Identifier 'defineComponent' has already been declared | Importado manualmente + inyectado por el plugin | No importar defineComponent en el <script> |
| Failed to load transformWithEsbuild | Vite 6+ eliminó esa función | Usar vite@5 |
| Tailwind no aplica estilos (texto plano) | Falta @tailwindcss/vite en plugins o usás directivas v3 | Agregar a vite.config.ts + usar @import "tailwindcss" |
| ReferenceError: X is not defined en template | Props no están en scope directo del template | Usar props.X en vez de X |
| state.X siempre undefined | signal() recibió un objeto en vez de un primitivo | signal(false) no signal({ x: false }) |
Stack
- TypeScript estricto
- Vite 5.x (build + dev server)
- Vitest + happy-dom (tests)
- Bun (package manager recomendado)
APIs públicas (barrel arckode-ui)
| Categoría | Exports |
|-----------|---------|
| Reactividad | signal, computed, watch, effect, Signal, ComputedSignal, StopFn |
| Componentes | defineComponent, onMount, onUnmount, onUpdate, onError, ErrorHook, h, mount, mountComponent |
| Estado global | defineStore, StoreInstance, UseStoreFn, PersistOptions |
| HTTP | createService, ArkServiceError, ServiceDefinition, ServiceInstance, RequestOptions |
| Router | createRouter, createRouterView, navigate, navigateTo, useRoute, getCurrentPath, RouteDefinition, RouteWithComponent, LazyComponent, RouteInfo |
| Testing | mountForTest, ArkTestWrapper |
| Analyzer | FixOp |
Subpath: arckode-ui/vite exporta arkcodeUi(options) y generateScopeId(filePath).
Estado
817 tests pasando · 35+ reglas del analyzer · 52 primitivos UI + 4 stores + 3 composables scaffoldeados por ark new.
Suprimir reglas del analyzer puntualmente
Para edge cases legítimos (demo con markup nativo, archivo vendor, etc.):
// .ts (composable, store, service)
// arckode-disable HIGH_CYCLOMATIC_COMPLEXITY
export function classify(n: number) { /* ... */ }<!-- .ark -->
<!-- arckode-disable PRIMITIVE_INLINE -->
<template>
<button class="custom">Necesito markup nativo</button>
</template>Las reglas suprimidas se listan separadas por espacio. Aplica solo al archivo. Usar con criterio — las reglas existen por una razón.
Licencia
MIT
